owin oauth webapi with a dynamic TokenEndpointPath (request for example) - owin

I refer to post of David Snipp in https://stackoverflow.com/a/29706891/4609861 to kindly ask a little example of his explanation; I am having troubles calling delegate OnMatchEndpoint and the MatchesTokenEndpoint. thx

You can specify the delegates on Options itself in your Startup.cs code. Something along these lines:
OAuthAuthorizationServerOptions options = new OAuthAuthorizationServerOptions()
{
provider = new OAuthAuthorizationServerProvider()
{
OnMatchEndpoint = (context =>
{ // do your stuff
context.MatchesAuthorizationEndpoint();
return Task.FromResult<object>(0);
});
}
}

Related

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);

ASP.NET core WEB API with AD FS authentication

I have small RESTful API written in ASP.NET core and I am looking into how to add authentication to it using Active Directory. I have to use our companies server for authentication using AD but I do not see any tutorials of how to do this.
I guess that JWT authentication is not what I am looking for or I might be wrong and misunderstand something. I am total noob in question of authentication.
I know we have solved that in one of the nodejs project of ours and it was not that straight forward. I would appreciate any help in that matter.
As of today, System.DirectoryServices hasn't been implemented in ASP.NET Core yet, but we could use Novell.Directory.Ldap.NETStandard.
You can install the package via NuGet Novell.Directory.Ldap.NETStandard.
Sample code -
using Novell.Directory.Ldap;
namespace YourNamespace
{
public class LdapAuthenticationService : IAuthenticationService
{
public bool ValidateUser(string domainName, string username, string password)
{
string userDn = $"{username}#{domainName}";
try
{
using (var connection = new LdapConnection {SecureSocketLayer = false})
{
connection.Connect(domainName, LdapConnection.DEFAULT_PORT);
connection.Bind(userDn, password);
if (connection.Bound)
return true;
}
}
catch (LdapException ex)
{
// Log exception
}
return false;
}
}
}

Swagger authentication in Azure App Service

In my Azure Mobile .NET backend I want to use Azure Mobile .NET Server Swagger . I'm looking for fast way to hide swagger UI from public access ? Is there any way to provide access only for selected users ?
First a disclaimer: Even if you protect your Swagger UI from public consumption, you are not protecting your APIs from public consumption. You have to assume that everyone knows all of your routes and have the appropriate security in place to protect any requests that may come in.
That being said, there's still not a simple way to do this. Swashbuckle (the piece that adds Swagger to Web API) adds a custom HttpMessageHandler to the /swagger/ui route (as seen here). If you look at the Web API pipeline, you can see that if you specify a custom handler, you can bypass all of the Controller selection, Auth filters, etc. This is what happens here.
Some solutions:
Use an app setting to conditionally call ConfigureSwagger(config) in debug modes only. This would prevent all /swagger routes from making it into production. Or you could use a staging slot and only add it there.
You can wrap the SwaggerUiHandler with something like this Basic Auth MessageHandler. This would prompt the user for basic creds if they went to the /swagger/ui route. See below for my modified version of this code.
Maybe with a little more thought we can come up with a better solution -- I see a couple of issues (here and here) in the Swashbuckle repo that indicate you're not the first one to hit this.
Modified BasicAuthHandler (from here):
Warning: minimally tested (and be sure to change how you verify user/pass)
public class BasicAuthMessageHandler : DelegatingHandler
{
private const string BasicAuthResponseHeader = "WWW-Authenticate";
private const string BasicAuthResponseHeaderValue = "Basic";
public BasicAuthMessageHandler(HttpMessageHandler innerHandler)
{
this.InnerHandler = innerHandler;
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
AuthenticationHeaderValue authValue = request.Headers.Authorization;
HttpResponseMessage unauthorizedResponse = request.CreateUnauthorizedResponse();
if (authValue != null && !string.IsNullOrWhiteSpace(authValue.Parameter))
{
Credentials parsedCredentials = ParseAuthorizationHeader(authValue.Parameter);
if (parsedCredentials != null)
{
// TODO: Check that the user/pass are valid
if (parsedCredentials.Username == "user" &&
parsedCredentials.Password == "pass")
{
// If match, pass along to the inner handler
return base.SendAsync(request, cancellationToken);
}
}
}
else
{
// Prompt for creds
unauthorizedResponse.Headers.Add(BasicAuthResponseHeader, BasicAuthResponseHeaderValue);
}
return Task.FromResult(unauthorizedResponse);
}
private Credentials ParseAuthorizationHeader(string authHeader)
{
string[] credentials = Encoding.ASCII.GetString(Convert
.FromBase64String(authHeader))
.Split(
new[] { ':' });
if (credentials.Length != 2 || string.IsNullOrEmpty(credentials[0])
|| string.IsNullOrEmpty(credentials[1])) return null;
return new Credentials()
{
Username = credentials[0],
Password = credentials[1],
};
}
}
Registering with Swagger route
// Do this after calling ConfigureSwagger
ConfigureSwagger(config);
// Remove the swagger_ui route and re-add it with the wrapped handler.
var route = config.Routes["swagger_ui"];
config.Routes.Remove("swagger_ui");
config.Routes.MapHttpRoute("swagger_ui", route.RouteTemplate, route.Defaults, route.Constraints, new BasicAuthMessageHandler(route.Handler));

owin oauth webapi with a dynamic TokenEndpointPath

I've successfully implemented oAuth using OWIN in my WebApi 2 Server with:
app.UseOAuthAuthorizationServer(new OAuthAuthorizationServerOptions {
TokenEndpointPath = new PathString("/api/TokenByPassword"),
// ...
});
However, I would like the TokenEndpointPath to be dynamic as I will have multiple databases each with their own account records.
I believe I want something like:
TokenEndpointPath = new PathString("/api/{databaseid}/TokenByPassword");
I don't believe OAuthAuthorizationServerOptions supports this and even if it did - how would I get the databaseid ?
I could implement this in my own WebAPI with AttributeRouting, but then what would be the correct OWIN calls to make in that WebAPI to generate the correct BearerToken?
I found the answer..
Even though the TokenEndpointPath is specified in the OAuthAuthorizationServerOptions, the OAuthAuthorizationServerProvider has a delegate called OnMatchEndpoint. Inside this delegate, you can access the Request.Uri.AbsolutePath of the call and if it matches your criteria, you can then call MatchesTokenEndpoint() in which case OnGrantResourceOwnerCredentials will get called where you again can gain access the the Request.Uri and pick out the {databaseid} and use the correct database to Grant access.
OWIN is very flexible, but not immediately obvious which calls to make when to do what you want when it is something not quite straightforward.
Just to make it clearer, here is the implementation of the function MatchEndpoint of the class that extend OAuthAuthorizationServerProvider, as suggested by David Snipp :
private const string MatchTokenUrlPattern = #"^\/([\d\w]{5})\/token\/?$";
public override async Task MatchEndpoint(OAuthMatchEndpointContext context)
{
var url = context.Request.Uri.AbsolutePath;
if (!string.IsNullOrEmpty(url) && url.Contains("token"))
{
var regexMatch = new Regex(MatchTokenUrlPattern).Match(url);
if (regexMatch.Success)
{
context.MatchesTokenEndpoint();
return;
}
}
await base.MatchEndpoint(context);
}
Be careful on what you do in there because it is called at every request.

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