Nancy.Owin vs Nancy.Hosting.Self with topshelf - owin

1) What is the differences between setting up with the two different packages? Is one more supported than the other?
I had a project using Nancy.Hosting.Self and didn't find any article about how to setup windows authentication, while I did find it for Nancy.Owin as middleware. I have now switched to Nancy.owin.
Here are a few different questions.
2) The jsonformatter can be configured in both the nancy bootstrapper as in the owin endpoint topshelf.
Where should I configure the formatter? If configured in the topshelf owin endpoint, will this be applied in the nancy bootstrapper as well?
3) With the nancy endpoint in topshelf there was an option to set firewall rules and url reservations. I can't find this in the owin endpoint.
private static void CreateHost(HostConfigurator host)
{
var logger = SetupLogging();
host.UseSerilog(logger);
host.UseLinuxIfAvailable();
//sc => serviceconfigurator, ls => licenseService
host.Service<WebService>(sc =>
{
sc.ConstructUsing(name => new WebService(_config));
sc.WhenStarted(ls => ls.Start());
sc.WhenStopped(ls => ls.Stop());
sc.OwinEndpoint(app =>
{
app.ConfigureHttp(configuration =>
{
//use json.net serializer
configuration.Formatters.Clear();
configuration.Formatters.Add(new JsonMediaTypeFormatter());
//configure json settings
var jsonSettings = configuration.Formatters.JsonFormatter.SerializerSettings;
jsonSettings.Formatting = Formatting.Indented;
jsonSettings.Converters.Add(new StringEnumConverter { CamelCaseText = false, AllowIntegerValues = true });
jsonSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
});
app.ConfigureAppBuilder(builder =>
{
builder.UseNancy(options =>
{
options.Bootstrapper = new Bootstrapper(_config);
//options.PerformPassThrough
});
});
app.ConfigureStartOptions(options =>
{
options.Port = _config.LicenseServicePort;
//options.Urls = new List<string>(){};
});
});
//add host reservation during service install, this is the only time, nancy will have admin rights, will be deleted when service is uninstalled as well.
//nc => nancyConfig
//sc.WithNancyEndpoint(host, nc =>
//{
// nc.AddHost(port: _config.LicenseServicePort);
// nc.CreateUrlReservationsOnInstall();
// nc.DeleteReservationsOnUnInstall();
// nc.OpenFirewallPortsOnInstall(firewallRuleName: "mycustomservice");
// nc.Bootstrapper = new Bootstrapper(_config);
//});
});
host.SetDescription("Licensing service for my api.");
host.SetDisplayName("myservice");
host.SetServiceName("myservice);
host.RunAsNetworkService();
host.StartAutomatically();
}

Posted answer for codetoshare
private static void CreateHost(HostConfigurator host)
{
Log.Logger = SetupLogging();
host.SetStartTimeout(TimeSpan.FromSeconds(60));
//Plug serilog into host: log startup urls and start / stop
host.UseSerilog(Log.Logger);
//Allow to be run on linux as well
host.UseLinuxIfAvailable();
//sc => serviceconfigurator, ls => licenseService
host.Service<WebService>(sc =>
{
//basic topshelf configuration
sc.ConstructUsing(name => new WebService());
sc.WhenStarted(ls => ls.Start());
sc.WhenStopped(ls =>
{
ls.Stop();
DisposeLogging();
});
//I am using an extension here because I had converted the application from Nancy.Host.Self to Nancy.Owin
//if the extension will not get updated and this breaks the application, convert it to a normal app: see nearly every topshelf + owin example
//owin configuration
sc.OwinEndpoint(app =>
{
app.ConfigureHttp(configuration =>
{
//use json.net serializer
configuration.Formatters.Clear();
configuration.Formatters.Add(new JsonMediaTypeFormatter());
//configure json settings
var jsonSettings = configuration.Formatters.JsonFormatter.SerializerSettings;
jsonSettings.Formatting = Formatting.Indented;
jsonSettings.Converters.Add(new StringEnumConverter { CamelCaseText = false, AllowIntegerValues = true });
jsonSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
});
app.ConfigureAppBuilder(builder =>
{
//setup windows authentication
HttpListener listener = (HttpListener)builder.Properties["System.Net.HttpListener"];
listener.AuthenticationSchemes = AuthenticationSchemes.IntegratedWindowsAuthentication;
//setup nancy
builder.UseNancy(options =>
{
options.Bootstrapper = new Bootstrapper(_config);
});
});
//setup urls: always add localhost and 127.0.0.1 together with the host specified in the config file
app.ConfigureStartOptions(options =>
{
options.Port = _config.LicenseServicePort;
var localhost = $"http://localhost:{_config.LicenseServicePort}";
var localhost2 = $"http://127.0.0.1:{_config.LicenseServicePort}";
//todo: this should support https as well
//todo: allow multiple hosts to be specified on config
options.Urls.Add(localhost);
options.Urls.Add(localhost2);
var configuredHost = $"{_config.LicenseServiceUrl}:{_config.LicenseServicePort}";
if (!configuredHost.Equals(localhost) && !configuredHost.Equals(localhost2))
{
options.Urls.Add(configuredHost);
}
});
});
//old nancyhost config, keep this untill documented on confluence
//add host reservation during service install, this is the only time, nancy will have admin rights, will be deleted when service is uninstalled as well.
//nc => nancyConfig
//sc.WithNancyEndpoint(host, nc =>
//{
// nc.AddHost(port: _config.LicenseServicePort);
// nc.CreateUrlReservationsOnInstall();
// nc.DeleteReservationsOnUnInstall();
// nc.OpenFirewallPortsOnInstall(firewallRuleName: "SegreyLicensingService");
// nc.Bootstrapper = new Bootstrapper(_config);
//});
});

Related

Call ASP .NET Core API in Azure from daemon application

I have limited experience on Azure AD and authentication mechanism. So far I cannot figure out why is not working. Here is the scenario:
I have an ASP net core 2.1 application deployed in azure web app service.
For authentication I’m using Open ID connect with .AddOpenIdConnect and provide client_id, secret_id, etc. When users are accessing my web API they are redirected to Microsoft login.
Now I need to expose an API to a third party application (scheduled web job) which is not in Azure.
I tried to use this sample from Microsoft, only the console app, as I already have the WebApp in Azure.
Running the sample I’m able to get the token, but when I call my API the response is the HTML to Microsoft login page.
On Azure portal on
Enterprise Application -> daemon-console -> Activity -> Service Principal sign-ins
I can see the success sign in.
Note: for testing I run the web app on my local machine and from the console application I’m calling API https://localhost:44306/api/test.
Asp .net core app:
services.AddAuthentication(option =>
{
option.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
option.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
option.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(option =>
{
option.Cookie.Name = "myWebApp";
option.Cookie.SecurePolicy = CookieSecurePolicy.Always;
option.Cookie.SameSite = SameSiteMode.None;
})
.AddOpenIdConnect(option =>
{
option.ClientId = client_id;
option.ClientSecret = client_secret;
option.Authority = authority;
option.SignedOutRedirectUri = "http://localhost:44306/";
option.CorrelationCookie.Name = "myWebAppCorrelation";
option.CorrelationCookie.SecurePolicy = CookieSecurePolicy.Always;
option.NonceCookie.Name = "WebAppNonce";
option.NonceCookie.SecurePolicy = CookieSecurePolicy.Always;
option.Resource = "https://graph.windows.net";
option.ResponseType = "id_token code";
})
Console app trying to access the API ( code extracted from Microsoft sample )
app = ConfidentialClientApplicationBuilder.Create(config.ClientId)
.WithClientSecret(config.ClientSecret)
.WithAuthority(new Uri(config.Authority))
.Build();
result = await app.AcquireTokenForClient(scopes).ExecuteAsync(); // ok
var httpClient = new HttpClient();
var defaultRequestHeaders = httpClient.DefaultRequestHeaders;
if (defaultRequestHeaders.Accept == null || !defaultRequestHeaders.Accept.Any(m => m.MediaType == "application/json"))
{
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
}
defaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
HttpResponseMessage response = await httpClient.GetAsync(webApiUrl);
if (response.IsSuccessStatusCode) // ok
{
string json = await response.Content.ReadAsStringAsync(); // here I'm getting the HTML to login page
var result = JsonConvert.DeserializeObject<List<JObject>>(json);
Console.ForegroundColor = ConsoleColor.Gray;
processResult(result);
}
The only difference between the sample code and my scenario is that the web app from sample is using services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddMicrosoftIdentityWebApi(Configuration)
but I cannot use .AddMicrosoftIdentityWebApi in Asp .Net core 2.1
Does anyone has an idea where the issue might be ? Do I need to add another authentication scheme ?
You need to support JWT authentication in addition to cookie authentication. So you need to add AddJwtBearer. You also need to extend the authentication check because you are now supporting multiple schemes.
This is how I would do it:
// public void ConfigureServices(IServiceCollection services)
services.AddAuthentication(option =>
{
option.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
option.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
option.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, cfg => {
cfg.Authority = authority;
cfg.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidAudience = /*see scopes of your deamon app*/
};
})
.AddCookie(option =>
{
// ...
})
.AddOpenIdConnect(option =>
{
// ...
})
// public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
app.UseAuthentication();
// https://github.com/aspnet/Security/issues/1847
app.Use(async (context, next) =>
{
if (!context.User.Identity.IsAuthenticated)
{
var result = await context.AuthenticateAsync(JwtBearerDefaults.AuthenticationScheme);
if (result.Succeeded)
{
context.User = result.Principal;
}
}
await next();
});
app.UseAuthorization();

identity server4, differents behaviors of login locally / azure

Environments : localhost / azure, .netcore 3.1 mvc identityserver4 + mvc api client.
When I run my application locally, the login/logout works fine, there is :
- an identityserver4 mvc .netcore 3.1
- a client mvc api .netcore 3.1
I can login / logout as much as I want, the login always redirects to the identityserver4 login and the login works.
When the same application with the identityserver4 hosted on Azure
The first login correctly redirects to the azure identityserver4, and login works fine.
Then after the logout (cockies seem to be removed), when I try login again, the redirection to the login page doesn't work and there is an "implicit" login and a direct redirection to the homepage of the website.
The client mvc api is configured like this :
{
"ClientId": "IdentityServer.WebApi",
"ClientSecret": "IdentityServer.WebApi",
"AllowedGrantTypes": "GrantTypes.CodeAndClientCredentials",
"RedirectUris": [
"https://localhost:44372/signin-oidc",
"https://localhost:5001/signin-oidc",
"https://192.168.1.7:44372/signin-oidc",
"https://mogui:44372/signin-oidc"
],
"PostLogoutRedirectUris": [
"https://localhost:44372/signout-callback-oidc",
"https://localhost:5001/signout-callback-oidc",
"https://192.168.1.7:44372/signout-callback-oidc",
"https://mogui:44372/signout-callback-oidc"
],
"AllowedScopes": [
"openid",
"profile"
],
"RequireConsent": true,
"RequirePkce": true,
"AllowOfflineAccess": true
},
The identityserver4 locally / on azure have this kind of code on its Startup class :
public void ConfigureServices(IServiceCollection services)
{
try
{
telemetryClient.TrackTrace("============== Startup ConfigureServices ============== ");
// uncomment, if you wan to add an MVC-based UI
services.AddControllersWithViews();
//services.AddMvc();
string connectionString = Configuration.GetConnectionString("IdentityDbContextConnection");
//const string connectionString = #"Data Source=(LocalDb)\MSSQLLocalDB;database=IdentityServer4.Quickstart.EntityFramework-3.0.102;trusted_connection=yes;";
var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
services.AddDbContext<IdentityServer.Models.IdentityDbContext>(options =>
options.UseSqlServer(connectionString, sql => sql.MigrationsAssembly(migrationsAssembly))
);
services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
options.SignIn.RequireConfirmedEmail = true;
})
.AddEntityFrameworkStores<IdentityServer.Models.IdentityDbContext>()
.AddDefaultTokenProviders();
services.AddMvc(options =>
{
options.EnableEndpointRouting = false;
})
.SetCompatibilityVersion(CompatibilityVersion.Latest);
var builder = services.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
options.UserInteraction.LoginUrl = "/Account/Login";
options.UserInteraction.LogoutUrl = "/Account/Logout";
options.Authentication = new AuthenticationOptions()
{
CookieLifetime = TimeSpan.FromHours(10), // ID server cookie timeout set to 10 hours
CookieSlidingExpiration = true
};
})
.AddSigningCredential(X509.GetCertificate("B22BBE7C991CEF13F470481A4042D1E091967FCC")) // signing.crt thumbprint
.AddValidationKey(X509.GetCertificate("321ABA505F6FCDDD00AA5EC2BD307F0C9002F9A8")) // validation.crt thumbprint
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = b => b.UseSqlServer(connectionString, sql => sql.MigrationsAssembly(migrationsAssembly));
})
.AddOperationalStore(options =>
{
options.ConfigureDbContext = b => b.UseSqlServer(connectionString, sql => sql.MigrationsAssembly(migrationsAssembly));
options.EnableTokenCleanup = true;
})
.AddAspNetIdentity<ApplicationUser>();
services.AddAuthentication()
.AddGoogle("Google", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.ClientId = "174637674775-7bgu471gtme25sr5iagq5agq6riottek.apps.googleusercontent.com";
options.ClientSecret = "V_UsR825ZWxCB9i2xrN-u1Kj";
});
services.AddTransient<IEmailSender, IdentityEmailSender>();
services.AddCors(options => options.AddPolicy("AllowAll", p => p.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()));
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.Strict;
});
services.AddScoped<IProfileService, ProfileService>();
telemetryClient.TrackTrace("============== Startup ConfigureServices finish OK ============== ");
}
catch (Exception e)
{
telemetryClient.TrackTrace("Exception general in ConfigureServices");
telemetryClient.TrackException(e);
throw;
}
}
and this :
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
try
{
telemetryClient.TrackTrace("============== Startup Configure ============== ");
InitializeDatabase(app);
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.UseCors("AllowAll");
app.UseRouting();
app.UseIdentityServer();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute();
});
app.UseMvcWithDefaultRoute();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
telemetryClient.TrackTrace("============== Startup Configure finish OK============== ");
}
catch (Exception e)
{
telemetryClient.TrackTrace("Exception general in Configure");
telemetryClient.TrackException(e);
throw;
}
}
So the problem is with
the identityserver4 localhost the login / logout works find
the idnetityserver4 hosted on azure the login is skipped and go diectly to the homepage (the user is authenticated with previous login).
Sorry to be a little long,
I haven't seen this exact problem on stackoverflow or somewhere else.
Thanx in advance !
There are several things that can go wrong with moving your app to production.
I suspect that if you are redirected back to your Homepage, that the auth cookies are not being removed by your SignOutAsync("Cookies) call.
Check these:
PostLogoutRedirectUris contain your azure domain + "signout-callback-oidc"
Check on what path is your Auth cookies created. If different from "/" - add the default path. I guess in your case it would be somewhat among the lines of:
options.Authentication = new AuthenticationOptions()
{
CookieLifetime = TimeSpan.FromHours(10), // ID server cookie timeout set to 10 hours
CookieSlidingExpiration = true,
Path = "/"
};
You're right Riste,
According to differents posts I've seen, we can do such a thing :
- first of all, we have to put FrontChannelLogoutUri parameter
(it should be an mvc client controller/action called by identityserver4, in our case
should be something like https://localhost:999/Account/FrontChannelLogout) for the client mvc app, generally it is put in Config.cs and add this parameter for the client Mvc (with RedirectUris, PostLogoutRedirectUris, ...)
- on the client mvc, in an account controller (for instance) where is managed the login ,
we can add / modifiy the logout management :
[Authorize]
public async Task<IActionResult> Logout()
{
var client = new HttpClient();
var disco = await client.GetDiscoveryDocumentAsync($"https://{Startup.Configuration["Auth0:Domain"]}");
return Redirect(disco.EndSessionEndpoint);
}
public async Task<IActionResult> FrontChannelLogout(string sid)
{
if (User.Identity.IsAuthenticated)
{
var currentSid = User.FindFirst("sid")?.Value ?? "";
if (string.Equals(currentSid, sid, StringComparison.Ordinal))
{
await HttpContext.SignOutAsync("oidc");
await HttpContext.SignOutAsync("Identity.Application");
await _signInManager.Context.SignOutAsync("_af");
await _signInManager.Context.SignOutAsync("idsrv.session");
await _signInManager.Context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
}
}
return NoContent();
}
On the identityserver4 side :
In the QuickStart Account Controller, we have to update the BuildLoggedOutViewModelAsync method :
private async Task<LoggedOutViewModel> BuildLoggedOutViewModelAsync(string logoutId)
{
// get context information (client name, post logout redirect URI and iframe for federated signout)
var logout = await _interaction.GetLogoutContextAsync(logoutId);
var client = await _clientStore.FindEnabledClientByIdAsync(logout.ClientIds.First());
if (!string.IsNullOrEmpty(client.FrontChannelLogoutUri))
{
//var pos = GetNthIndex(client.FrontChannelLogoutUri, '/', 3);
//logout.PostLogoutRedirectUri = client.FrontChannelLogoutUri.Substring(0, Math.Min(client.FrontChannelLogoutUri.Length, pos));
// Here TODO =====> get the real PostLogoutRedirectUri, it should be a controller/action url on the client mvc side and put it in **logout.PostLogoutRedirectUri**
}
var vm = new LoggedOutViewModel
{
AutomaticRedirectAfterSignOut = AccountOptions.AutomaticRedirectAfterSignOut,
PostLogoutRedirectUri = logout?.PostLogoutRedirectUri,
ClientName = string.IsNullOrEmpty(logout?.ClientName) ? logout?.ClientId : logout?.ClientName,
SignOutIframeUrl = logout?.SignOutIFrameUrl,
LogoutId = logoutId
};
if (User?.Identity.IsAuthenticated == true)
{
var idp = User.FindFirst(JwtClaimTypes.IdentityProvider)?.Value;
if (idp != null && idp != IdentityServer4.IdentityServerConstants.LocalIdentityProvider)
{
var providerSupportsSignout = await HttpContext.GetSchemeSupportsSignOutAsync(idp);
if (providerSupportsSignout)
{
if (vm.LogoutId == null)
{
// if there's no current logout context, we need to create one
// this captures necessary info from the current logged in user
// before we signout and redirect away to the external IdP for signout
vm.LogoutId = await _interaction.CreateLogoutContextAsync();
}
vm.ExternalAuthenticationScheme = idp;
}
}
}
return vm;
}
====> Apparently _interaction.GetLogoutContextAsync(logoutId) never return a PostLogoutRedirectUri even though it has been set up for the mvc client (in the Config.cs).
====> by filling this parameter logout.PostLogoutRedirectUri on identityServer4 side it'll redirect the logout to the client app.
Here is what I can say, I don't know if the logout redirect to the client app is a "standard" behavior, don't know if it was planned in identityserver4.
Some links :
https://andersonnjen.com/2019/03/22/identityserver4-global-logout/
How to redirect user to client app after logging out from identity server?
Thanx !

No sign-out authentication handler is registered for the scheme 'Identity.TwoFactorUserId'

ASP.NET Core 2.2 web app using code migrated from full fat MVC app.
My AccountController contains this simple code for its Logout route.
await this.SignInManager.SignOutAsync();
return this.RedirectToAction(nameof(Landing.HomeController.Index), "Home");
But this gives.
No sign-out authentication handler is registered for the scheme 'Identity.TwoFactorUserId'.
Pretty confusing given that I've never mentioned 2FA in my code, and Google login is working.
serviceCollection
.AddIdentityCore<MyUser>(identityOptions =>
{
identityOptions.SignIn.RequireConfirmedEmail = false;
})
.AddUserStore<MyUserStore>()
.AddSignInManager<SignInManager<MyUser>>();
serviceCollection.AddAuthentication(IdentityConstants.ApplicationScheme)
.AddCookie(IdentityConstants.ApplicationScheme, options =>
{
options.SlidingExpiration = true;
})
.AddGoogle(googleOptions =>
{
this.Configuration.Bind("OAuth2:Providers:Google", googleOptions);
googleOptions.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "sub", "string");
})
.AddExternalCookie();
As a complement to #Luke's answer:
The reason why SignInManager::SignOutAsync() throws is this method will also sign out the TwoFactorUserIdScheme behind the scenes:
public virtual async Task SignOutAsync()
{
await Context.SignOutAsync(IdentityConstants.ApplicationScheme);
await Context.SignOutAsync(IdentityConstants.ExternalScheme);
await Context.SignOutAsync(IdentityConstants.TwoFactorUserIdScheme);
}
(See source code)
Typically, these tree authentication schemes are registered automatically by AddIdentity<TUser, TRole>():
public static IdentityBuilder AddIdentity<TUser, TRole>(
this IServiceCollection services,
Action<IdentityOptions> setupAction)
{
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme;
options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
})
...
.AddCookie(IdentityConstants.TwoFactorUserIdScheme, o =>
{
o.Cookie.Name = IdentityConstants.TwoFactorUserIdScheme;
o.ExpireTimeSpan = TimeSpan.FromMinutes(5);
});
... // other services
}
(See source code )
However, you added the Identity services by AddIdentityCore<>() instead of the AddIdentity<>().
Because the AddIdentityCore<>() doesn't register a TwoFactorUserIdScheme scheme (see source code) automatically, there's no associated CookieAuthenticationHandler for TwoFactorUserIdScheme. As a result, it throws.
How to solve
In order to work with SignInManager.SignOutAsync(), according to above description, we need ensure a <scheme>-<handler> map has been registed for TwoFactorUserIdScheme .
So I change your code as below, now it works fine for me:
serviceCollection.AddAuthentication(IdentityConstants.ApplicationScheme)
.AddCookie(IdentityConstants.ApplicationScheme, options =>
{
options.SlidingExpiration = true;
})
.AddCookie(IdentityConstants.TwoFactorUserIdScheme, o =>
{
o.Cookie.Name = IdentityConstants.TwoFactorUserIdScheme;
o.ExpireTimeSpan = TimeSpan.FromMinutes(5);
})
.AddGoogle(googleOptions =>
{
this.Configuration.Bind("OAuth2:Providers:Google", googleOptions);
googleOptions.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "sub", "string");
})
.AddExternalCookie();
Also you can create your own SignInManager<MyUser> and override sign out as you need
public class CustomSignInManager : SignInManager<MyUser>
{
public override async Task SignOutAsync()
{
await Context.SignOutAsync(IdentityConstants.ApplicationScheme);
await Context.SignOutAsync(GoogleDefaults.AuthenticationScheme);
}
}
Then change AddSignInManager<SignInManager<MyUser>>() to AddSignInManager<CustomSignInManager>() in your Startup class
serviceCollection
.AddIdentityCore<MyUser>(identityOptions =>
{
identityOptions.SignIn.RequireConfirmedEmail = false;
})
.AddUserStore<MyUserStore>()
.AddSignInManager<CustomSignInManager>();
Do not use the SignOutAsync method on a SignInManager<T> you've injected into the controller. Instead, use the method on the HttpContext which takes a scheme argument. I don't know why.
Below code works for me , use the same AuthenticationScheme that you use while "AddAuthentication" in startup.cs
[HttpGet("signout")]
[AllowAnonymous]
public async Task signout()
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
var prop = new AuthenticationProperties
{
RedirectUri = "/logout-complete"
};
// after signout this will redirect to your provided target
await HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme, prop);
}
[HttpGet("logout-complete")]
[AllowAnonymous]
public string logoutComplete()
{
return "logout-complete";
}
I agree with itminus reply, We would get an error because in .net core 3.0 if we use AddIdentityCore<>() instead of AddIdentity<>() we would get an error. But when upgrading to .net 7.0 if we again use AddIdentityCore<>() instead of AddIdentity<>() we would get the same error for Identity.TwoFactorRememberMeScheme as well that I am faced with after upgrading. For SignInManager we require Identity.TwoFactorRememberMeScheme as well otherwise we get an error.
The solution which I applied in mine .net 7.0 project is:
Instead of adding every scheme and handler by yourself, we can just use
services.AddAuthentication().AddIdentityCookies();
This will add all the schemes handlers for you and at the time of signout we need to should use below:
await HttpContext.SignOutAsync(IdentityConstants.ApplicationScheme);
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
await HttpContext.SignOutAsync(IdentityConstants.TwoFactorUserIdScheme);

ServiceStack minimum configuration to get Redis Pub/Sub working between multiple Web sites/services

Let's say for sake of argument I have 3 web service hosts running, and only one of them has registered any handlers (which I think equates to subscribing to the channel/topic) e.g.
var mqService = new RedisMqServer(container.Resolve<IRedisClientsManager>())
{
DisablePriorityQueues = true
};
container.Register<IMessageService>(mqService);
container.Register(mqService.MessageFactory);
mqService.RegisterHandler<OutboundInitiateCallInfo>(ServiceController.ExecuteMessage);
mqService.RegisterHandler<DirectMailAssignmentInfo>(ServiceController.ExecuteMessage);
mqService.Start();
Now my question is, "Do I need to construct the other app hosts in the same fashion if they only publish??" e.g.
var mqService = new RedisMqServer(container.Resolve<IRedisClientsManager>())
{
DisablePriorityQueues = true
};
container.Register<IMessageService>(mqService);
container.Register(mqService.MessageFactory);
mqService.Start(); <=== Do I need to start the service, or is the MessageFactory registration enough?
Thank you,
Stephen
The minimum code for a publisher is just:
var redisManager = container.Resolve<IRedisClientsManager>();
using (var mqProducer = new RedisMessageProducer(redisManager))
{
mqProducer.Publish(new Msg { ... });
}
You could also use a MessageFactory:
var msgFactory = new RedisMessageFactory(redisMangager);
using (var mqClient = msgFactory.CreateMessageQueueClient())
{
mqClient.Publish(new Msg { ... });
}

How can I get my custom OWIN auth working in Azure Mobile Service

I have a working Web API 2 mobile service hosted in AWS and I want to move to AMS. It works in Postman and on mobile devices just fine.
I followed several blog/posts and spent several hours rewriting and reordering the WebApiConfig.Register. I then created a new AMS project and copied over all my controllers etc. and I had the same result. I reviewed many similar questions but am brain dead over 20 something lines of code.
It works locally through Postman but after I published it I get
HTTP 401 - {"message":"Authorization has been denied for this request."}
Here is the AWS working startup.cs -- I do not call WebApiConfig.Register
namespace Savviety.Data.Service
{
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
var config = new HttpConfiguration();
ConfigureOAuth(app);
// remove in production
var cors = new EnableCorsAttribute("*", "*", "*");
config.EnableCors(cors);
app.UseWebApi(config);
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
var path = AppDomain.CurrentDomain.BaseDirectory + #"\log4net.config";
var fileInfo = new FileInfo(path);
XmlConfigurator.ConfigureAndWatch(fileInfo);
if (fileInfo.Exists)
{
log4net.Config.XmlConfigurator.ConfigureAndWatch(fileInfo);
}
else
{
throw new FileNotFoundException("Could not find log4net.config");
}
}
public void ConfigureOAuth(IAppBuilder app)
{
var oAuthServerOptions = new OAuthAuthorizationServerOptions()
{
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
Provider = new SimpleAuthorizationServerProvider()
};
// Token Generation
app.UseOAuthAuthorizationServer(oAuthServerOptions);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
}
}
In the AMS version I call the WebApiConfig.Register method from Application.Onstart in Global.asax
public static void Register( )
{
.
var options = new ConfigOptions();
var config = ServiceConfig.Initialize(new ConfigBuilder(options));
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
// Make sure this is after ServiceConfig.Initialize
// Otherwise ServiceConfig.Initialize will overwrite your changes
Microsoft.WindowsAzure.Mobile.Service.Config.StartupOwinAppBuilder.Initialize(appBuilder =>
{
ConfigureOAuth(appBuilder);
appBuilder.UseWebApi(config);
var path = AppDomain.CurrentDomain.BaseDirectory + #"\log4net.config";
var fileInfo = new FileInfo(path);
});
//var cors = new EnableCorsAttribute("*", "*", "*");
//config.EnableCors(cors);
// Web API routes
// config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
I also replaced [Authorize] with [AuthorizeLevel(AuthorizationLevel.User)] and removed the startup.cs class.
In Postman it works locally, but not after I publish it. It generates a token, but authentication fails.
TIA
Gary
The AuthorizeLevel attribute looks for a token issued by Mobile Services. Since you are not actually issuing such a token in the above, it will fail.
Things are probably working locally since the default config makes all local calls accepted. As described here, you will want to go into the Register() method of WebApiConfig.cs and add the following:
config.SetIsHosted(true);
This should cause calls to start failing locally.
To address the core issue, it is possible to wire your own OWIN provider into the Mobile Services pipeline. You will need to create a child class of LoginProvider which basically does your ConfigureAuth() call inside of its ConfigureMiddleware(). Please see the example in this blog post which sets up a LinkedIn middleware.
Ok, the primary issue is Azure will not support custom OWIN authentication or I cannot find how to implement it anywhere. I have to use a provided list of users and passwords from another system so it has to be custom.
The solution is a custom LoginController and LoginProvider the relevant code is below.
MyLoginProvider is a subclass of LoginProvider and calls the CreateLoginResult base method.
I had to modify my javascript auth interceptor to config.headers["X-ZUMO-AUTH"] = $localStorage.token; instead of the OAuth bearer token header.
I cannot get the email or display name from the claims identity on a request but I used a work around. When I figure it out I will post it here, but for now it is not blocking me.
public HttpResponseMessage Post(LoginRequest loginRequest)
{
var mongoDbManager = MongoDbManager.GetInstance();
var userCollection = mongoDbManager.GetCollection<UserDocument>(CollectionNames.User);
var q0 = Query<UserDocument>.EQ(i => i.ClientId, loginRequest.ClientId);
var q1 = Query<UserDocument>.EQ(i => i.UserEmailAddress, loginRequest.UserName);
var q2 = Query<UserDocument>.EQ(i => i.UserPassword, loginRequest.Password);
var query = Query.And(q0, q1, q2);
var result = userCollection.FindOne(query);
if (result == null)
{
return this.Request.CreateResponse(HttpStatusCode.Unauthorized, "Invalid username or password");
}
else
{
var claimsIdentity = new ClaimsIdentity();
claimsIdentity.AddClaim(new Claim(ClaimTypes.NameIdentifier, result.UserId));
claimsIdentity.AddClaim(new Claim(ClaimTypes.Email, result.UserEmailAddress));
claimsIdentity.AddClaim(new Claim("DisplayName", result.DisplayName));
var loginResult = new SavvietyLoginProvider(handler).CreateLoginResult(claimsIdentity, Services.Settings.MasterKey);
return this.Request.CreateResponse(HttpStatusCode.OK, loginResult);
}
}
}

Resources