Using email in token request with IdentityServer4 - asp.net-core

I'm usin ROPC flow with IdentityServer4 with ASP.NET Core Identity where I need to send username and password to /connect/token endpoint to get access_token.
How to configure IdentityServer4 to accept either Username or Email, and password in /connect/token requests?
PS: asp.net core: 2.0.2
IdentityServer4: 2.0.2

Found this solution:
1) Copy file IdentityServer4.AspNetIdentity/src/IdentityServer4.AspNetIdentity/ResourceOwnerPasswordValidator.cs to your project from https://github.com/IdentityServer/IdentityServer4.AspNetIdentity/blob/dev/src/IdentityServer4.AspNetIdentity/ResourceOwnerPasswordValidator.cs
2) Fix ValidateAsync method to find user with email
var user = await _userManager.FindByNameAsync(context.UserName);
if (user == null) {
user = await _userManager.FindByEmailAsync(context.UserName);
}
3) Add Validator to IS4:
.AddAspNetIdentity<AppUser>()
.AddResourceOwnerValidator<Services.ResourceOwnerPasswordValidator<AppUser>>();
4) Profit!

You need to override the ValidateAsync by creating the Custom PasswordValidator class which implement IResourceOwnerPasswordValidator interface like below.
public class OwnerPasswordValidatorService : IResourceOwnerPasswordValidator
{
public UserManager<ApplicationUser> _userManager { get; }
public OwnerPasswordValidatorService(UserManager<ApplicationUser> userManager)
{
_userManager = userManager;
}
public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
{
var user = await _userManager.FindByNameAsync(context.UserName);
if (user == null)
{
user = await _userManager.FindByEmailAsync(context.UserName);
if (user == null)
{
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Username or password is incorrect");
return;
}
}
var passwordValid = await _userManager.CheckPasswordAsync(user, context.Password);
if (!passwordValid)
{
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Username or password is incorrect");
return;
}
context.Result = new GrantValidationResult(subject: context.UserName, authenticationMethod: "custom");
}
}
Then configure your dependency for your custom class like below.
services.AddIdentityServer()
.AddDeveloperSigningCredential()
...
.AddAspNetIdentity<ApplicationUser>()
.AddResourceOwnerValidator<OwnerPasswordValidatorService>() //here
.AddProfileService<ProfileService>();
If any one still cannot make it work let me know. I will be happy to help.

Related

Blazor wasm get additional information and add to user claims

I am using identityserver4 for authentication and it's laid out something like this: identity server4 -> Web Api -> Blazor WASM Client(Standalone). everything is getting authenticated and working great. I get the authenticated user claims all the way to the wasm client.
I am now trying to add more claims which come directly from the database. I could have added the claims to the identityserver token but the token gets too big (> 2kb) and then identityserver stops working. apparently this is a known issue.
So iwant to build authorization and trying to keep the jwt token from identityserver small.
in the program.cs file i have a http client like so
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddOidcAuthentication(options =>// Configure your authentication provider options here.
// For more information, see https://aka.ms/blazor-standalone-auth
//builder.Configuration.Bind("Local", options.ProviderOptions);
... provider options
options.ProviderOptions.ResponseType = "code";
options.UserOptions.RoleClaim = "role";
}).AddAccountClaimsPrincipalFactory<CustomAccountClaimsPrincipalFactory>();
await builder.Build().RunAsync();
in the file CustomAccountClaimsPrincipalFactory i have this
public class CustomAccountClaimsPrincipalFactory
: AccountClaimsPrincipalFactory<RemoteUserAccount>
{
private const string Planet = "planet";
[Inject]
public HttpClient Http { get; set; }
public CustomAccountClaimsPrincipalFactory(IAccessTokenProviderAccessor accessor)
: base(accessor) {
}
public async override ValueTask<ClaimsPrincipal> CreateUserAsync(
RemoteUserAccount account,
RemoteAuthenticationUserOptions options)
{
var user = await base.CreateUserAsync(account, options);
if (user.Identity.IsAuthenticated)
{
var identity = (ClaimsIdentity)user.Identity;
var claims = identity.Claims.Where(a => a.Type == Planet);
if (!claims.Any())
{
identity.AddClaim(new Claim(Planet, "mars"));
}
//get user roles
//var url = $"/Identity/users/112b7de8-614f-40dc-a9e2-fa6e9d2bf85a/roles";
var dResources = await Http.GetFromJsonAsync<List<somemodel>>("/endpoint");
foreach (var item in dResources)
{
identity.AddClaim(new Claim(item.Name, item.DisplayName));
}
}
return user;
}
}
this is not working as the httpclient is not biolt when this is called and the http client uses the same builder which is building the base http client.
How do i get this to work?
You can create a IProfileService and customise it however you need:
var builder = services.AddIdentityServer(options =>
...
.AddProfileService<IdentityProfileService>();
public class IdentityProfileService : IProfileService
{
private readonly IUserClaimsPrincipalFactory<ApplicationUser> _claimsFactory;
private readonly UserManager<ApplicationUser> _userManager;
public IdentityProfileService(IUserClaimsPrincipalFactory<ApplicationUser> claimsFactory, UserManager<ApplicationUser> userManager)
{
_claimsFactory = claimsFactory;
_userManager = userManager;
}
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var sub = context.Subject.GetSubjectId();
var user = await _userManager.FindByIdAsync(sub);
if (user == null)
{
throw new ArgumentException("");
}
var principal = await _claimsFactory.CreateAsync(user);
var claims = principal.Claims.ToList();
//Add more claims like this
//claims.Add(new System.Security.Claims.Claim("MyProfileID", user.Id));
context.IssuedClaims = claims;
}
public async Task IsActiveAsync(IsActiveContext context)
{
var sub = context.Subject.GetSubjectId();
var user = await _userManager.FindByIdAsync(sub);
context.IsActive = user != null;
}
}
Keep the access token small and only include the necessary claims to get past the JwtBearer authentication step.
Then in the API that receives an access token, you can simply create an authorization policy that do lookup the users additional claims and evaluate if he have access or not.
You can do that in the simple policy definitions or the more advanced authorization handlers like the code below:
public class CheckIfAccountantHandler : AuthorizationHandler<CanViewReportsRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
CanViewReportsRequirement requirement)
{
bool result = CallTheCheckIfAccountantService();
if(result)
context.Succeed(requirement);
return Task.CompletedTask;
}
}
A sample requirement can be defined as:
public class CanViewReportsRequirement : IAuthorizationRequirement
{
public int StartHour { get; }
public int EndHour { get; }
public CanViewReportsRequirement(int startHour, int endHour)
{
StartHour = startHour;
EndHour = endHour;
}
}
The important thing is to keep the complexity of the application low and not try to make it harder than it has to be. Just to make the system easy to reason about!

Adding a custom AuthenticationHandler to a list of external login providers

I'm using ASP.NET Core 3.1, and I have multiple external login providers configured:
services.AddAuthentication()
.AddDeviantArt(d => {
d.Scope.Add("feed");
d.ClientId = Configuration["Authentication:DeviantArt:ClientId"];
d.ClientSecret = Configuration["Authentication:DeviantArt:ClientSecret"];
d.SaveTokens = true;
})
.AddTwitter(t => {
t.ConsumerKey = Configuration["Authentication:Twitter:ConsumerKey"];
t.ConsumerSecret = Configuration["Authentication:Twitter:ConsumerSecret"];
t.SaveTokens = true;
});
I'd like to create a custom AuthenticationProvider that, instead of redirecting to another website, asks for that website's "API key" and treats that as the access token. (The website in question does not support any version of OAuth.)
Before actually hooking it up, I want to test that I can get a custom AuthenticationProvider working at all, so I found one that implements HTTP Basic authentication:
public class CustomAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions> {
public CustomAuthenticationHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock)
: base(options, logger, encoder, clock) { }
protected override async Task<AuthenticateResult> HandleAuthenticateAsync() {
if (!Request.Headers.ContainsKey("Authorization")) {
return AuthenticateResult.NoResult();
}
if (!AuthenticationHeaderValue.TryParse(Request.Headers["Authorization"], out AuthenticationHeaderValue headerValue)) {
return AuthenticateResult.NoResult();
}
if (!"Basic".Equals(headerValue.Scheme, StringComparison.OrdinalIgnoreCase)) {
return AuthenticateResult.NoResult();
}
byte[] headerValueBytes = Convert.FromBase64String(headerValue.Parameter);
string userAndPassword = Encoding.UTF8.GetString(headerValueBytes);
string[] parts = userAndPassword.Split(':');
if (parts.Length != 2) {
return AuthenticateResult.Fail("Invalid Basic authentication header");
}
string user = parts[0];
string password = parts[1];
bool isValidUser = true;
if (!isValidUser) {
return AuthenticateResult.Fail("Invalid username or password");
}
var claims = new[] { new Claim(ClaimTypes.Name, user) };
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return AuthenticateResult.Success(ticket);
}
protected override async Task HandleChallengeAsync(AuthenticationProperties properties) {
Response.Headers["WWW-Authenticate"] = $"Basic realm=\"Custom realm name here\", charset=\"UTF-8\"";
await base.HandleChallengeAsync(properties);
}
}
I added this to Startup.cs:
.AddScheme<AuthenticationSchemeOptions, CustomAuthenticationHandler>("Test", "Testing", o => { })
The problem is that the HandleAuthenticateAsync method is never called. The other solutions I find to this problem generally say you need to make it the "default" authentication scheme, but I don't want this to interfere with how the other external login providers are set up.
Is there a way I can get this working without changing the default authentication scheme or adding additional attributes to my controllers or actions?
You need add a default Scheme when you add AddAuthentication
like services.AddAuthentication("Test")

IdentityServer4 requesting a JWT / Access Bearer Token using the password grant in asp.net core

I've searched all over on requesting a JWT / Access Bearer Token using the password grant using IdentityServer4 in asp.net core, but I cant seem to find the right way to do it.
Below is the POST Request from which I register my user.
http://localhost:52718/account/register
Below is the Bearer Token GET Request from which I can get JWT Token using IdentityServer4
http://localhost:52718/connect/token
Below is the POST Request from which I Login my user
http://localhost:52718/account/signin
Now, what I'm trying to do is when I login my user then I want a JWT / Bearer Token same as I get from here http://localhost:52718/connect/token. When I hit this URL.
Here is my AccountController Code:
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Server.Models;
using Server.Models.AccountViewModels;
using Server.Models.UserViewModels;
namespace Server.Controllers
{
public class AccountController : Controller
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly RoleManager<IdentityRole> _roleManager;
public AccountController(
UserManager<ApplicationUser> userManager,
RoleManager<IdentityRole> roleManager
)
{
_userManager = userManager;
_roleManager = roleManager;
}
[HttpPost]
public async Task<IActionResult> Register([FromBody]RegisterViewModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var user = new ApplicationUser { UserName = model.UserName, FirstName = model.FirstName, LastName = model.LastName, Email = model.Email };
var result = await _userManager.CreateAsync(user, model.Password);
string role = "Basic User";
if (result.Succeeded)
{
if (await _roleManager.FindByNameAsync(role) == null)
{
await _roleManager.CreateAsync(new IdentityRole(role));
}
await _userManager.AddToRoleAsync(user, role);
await _userManager.AddClaimAsync(user, new System.Security.Claims.Claim("userName", user.UserName));
await _userManager.AddClaimAsync(user, new System.Security.Claims.Claim("firstName", user.FirstName));
await _userManager.AddClaimAsync(user, new System.Security.Claims.Claim("lastName", user.LastName));
await _userManager.AddClaimAsync(user, new System.Security.Claims.Claim("email", user.Email));
await _userManager.AddClaimAsync(user, new System.Security.Claims.Claim("role", role));
return Ok(new ProfileViewModel(user));
}
return BadRequest(result.Errors);
}
public async Task<IActionResult> Signin([FromBody]LoginViewModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var result = await _userManager.FindByNameAsync(model.UserName);
if (result != null && await _userManager.CheckPasswordAsync(result, model.Password))
{
return Ok(new ProfileViewModel(result));
}
return BadRequest("Invalid username or password.");
}
}
}
When I hit signin method I successfully get the data of user.
But I also need a jwt / access token when user login my app.
Now my actual question is:
What can I do in my signin method so when user login it returns me token along with other user data. I hope I briefly explain my question.
Thanks
I've found my own question answer. Before starting I show you my that code where I'm Defining the client.
public static IEnumerable<Client> GetClients()
{
// client credentials client
return new List<Client>
{
// resource owner password grant client
new Client
{
ClientId = "ro.angular",
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes = {
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
IdentityServerConstants.StandardScopes.Address,
"api1"
}
}
};
}
Now what I do in my Signin Method is to use the TokenClient class to request the token. To create an instance you need to pass in the token endpoint address, client id and secret.
Next I'm using Requesting a token using the password grant to allows a client to send username and password to the token service and get an access token back that represents that user.
Here is my Signin Code which I need to modify:
public async Task<IActionResult> Signin([FromBody]LoginViewModel model)
{
var disco = await DiscoveryClient.GetAsync("http://localhost:52718");
if (disco.IsError)
{
return BadRequest(disco.Error);
}
var tokenClient = new TokenClient(disco.TokenEndpoint, "ro.angular", "secret");
var tokenResponse = await tokenClient.RequestResourceOwnerPasswordAsync(model.UserName, model.Password, "api1 openid");
if (tokenResponse.IsError)
{
return BadRequest(tokenResponse.Error);
}
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var user = _userManager.FindByNameAsync(model.UserName);
var result = await _userManager.FindByNameAsync(model.UserName);
if (result != null && await _userManager.CheckPasswordAsync(result, model.Password))
{
return Ok(new ProfileViewModel(result, tokenResponse));
}
return BadRequest("Invalid username or password.");
}
Also I modify ProfileViewModel Class and add two new Token & Expiry:
public class ProfileViewModel
{
public string Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public string Token { get; set; }
public int Expiry { get; set; }
public ProfileViewModel()
{
}
public ProfileViewModel(ApplicationUser user, TokenResponse UToken = null)
{
Id = user.Id;
FirstName = user.FirstName;
LastName = user.LastName;
Email = user.Email;
Token = UToken.AccessToken;
Expiry = UToken.ExpiresIn;
}
public static IEnumerable<ProfileViewModel> GetUserProfiles(IEnumerable<ApplicationUser> users)
{
var profiles = new List<ProfileViewModel> { };
foreach (ApplicationUser user in users)
{
profiles.Add(new ProfileViewModel(user));
}
return profiles;
}
}
Now Here is my desire output. Hope this answer help others.

.net Core Authentication

I wanted to implement forms authentication with membership in my asp.net MVC Core application.
We had forms authentication setup in our previous application as below and wanted to use the same in .net core.
[HttpPost]
public ActionResult Login(LoginModel model, string returnUrl)
{
if (!this.ModelState.IsValid)
{
return this.View(model);
}
//Authenticate
if (!Membership.ValidateUser(model.UserName, model.Password))
{
this.ModelState.AddModelError(string.Empty, "The user name or
password provided is incorrect.");
return this.View(model);
}
else
{
FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe);
return this.RedirectToAction("Index", "Home");
}
return this.View(model);
}
In my config:
<membership defaultProvider="ADMembership">
<providers>
<add name="ADMembership"
type="System.Web.Security.ActiveDirectoryMembershipProvider"
connectionStringName="ADConnectionString"
attributeMapUsername="sAMAccountName" />
</providers>
</membership>
So we are using active directory here in membership.
Is this still applicable in .net core.
If not what else is available in .net core for forms authentication and AD.
Would appreciate inputs.
Yes you can do that in Core MVC application. You enable form authentication and use LDAP as user store at the back-end.
Here is how I set things up, to give you start:
Startup.cs
public class Startup
{
...
public void ConfigureServices(IServiceCollection services)
{
...
// Read LDAP settings from appsettings
services.Configure<LdapConfig>(this.Configuration.GetSection("ldap"));
// Define an interface for authentication service,
// We used Novell.Directory.Ldap as implementation.
services.AddScoped<IAuthenticationService, LdapAuthenticationService>();
// Global filter is enabled to protect the whole site
services.AddMvc(config =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
...
});
// Form authentication and cookies settings
var cookiesConfig = this.Configuration.GetSection("cookies").Get<CookiesConfig>();
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.Cookie.Name = cookiesConfig.CookieName;
options.LoginPath = cookiesConfig.LoginPath;
options.LogoutPath = cookiesConfig.LogoutPath;
options.AccessDeniedPath = cookiesConfig.AccessDeniedPath;
options.ReturnUrlParameter = cookiesConfig.ReturnUrlParameter;
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// Redirects all HTTP requests to HTTPS
if (env.IsProduction())
{
app.UseRewriter(new RewriteOptions()
.AddRedirectToHttpsPermanent());
}
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/error");
}
app.UseStaticFiles();
app.UseStatusCodePagesWithReExecute("/error", "?code={0}");
app.UseAuthentication();
app.UseMvc(routes =>
{
...
});
}
}
appsettings.json
{
"connectionStrings": {
"appDbConnection": xxx
},
"ldap": {
"url": "xxx.loc",
"bindDn": "CN=Users,DC=xxx,DC=loc",
"username": "xxx",
"password": "xxx",
"searchBase": "DC=xxx,DC=loc",
"searchFilter": "(&(objectClass=user)(objectClass=person)(sAMAccountName={0}))"
},
"cookies": {
"cookieName": "xxx",
"loginPath": "/account/login",
"logoutPath": "/account/logout",
"accessDeniedPath": "/account/accessDenied",
"returnUrlParameter": "returnUrl"
}
}
IAuthenticationService.cs
namespace DL.SO.Services.Core
{
public interface IAuthenticationService
{
IAppUser Login(string username, string password);
}
}
LdapAuthenticationService.cs
Ldap implementation of authentication service, using Novell.Directory.Ldap library to talk to active directory. You can Nuget that library.
using Microsoft.Extensions.Options;
using Novell.Directory.Ldap;
...
using DL.SO.Services.Core;
namespace DL.SO.Services.Security.Ldap
{
public class LdapAuthenticationService : IAuthenticationService
{
private const string MemberOfAttribute = "memberOf";
private const string DisplayNameAttribute = "displayName";
private const string SAMAccountNameAttribute = "sAMAccountName";
private const string MailAttribute = "mail";
private readonly LdapConfig _config;
private readonly LdapConnection _connection;
public LdapAuthenticationService(IOptions<LdapConfig> configAccessor)
{
// Config from appsettings, injected through the pipeline
_config = configAccessor.Value;
_connection = new LdapConnection();
}
public IAppUser Login(string username, string password)
{
_connection.Connect(_config.Url, LdapConnection.DEFAULT_PORT);
_connection.Bind(_config.Username, _config.Password);
var searchFilter = String.Format(_config.SearchFilter, username);
var result = _connection.Search(_config.SearchBase, LdapConnection.SCOPE_SUB, searchFilter,
new[] { MemberOfAttribute, DisplayNameAttribute, SAMAccountNameAttribute, MailAttribute }, false);
try
{
var user = result.next();
if (user != null)
{
_connection.Bind(user.DN, password);
if (_connection.Bound)
{
var accountNameAttr = user.getAttribute(SAMAccountNameAttribute);
if (accountNameAttr == null)
{
throw new Exception("Your account is missing the account name.");
}
var displayNameAttr = user.getAttribute(DisplayNameAttribute);
if (displayNameAttr == null)
{
throw new Exception("Your account is missing the display name.");
}
var emailAttr = user.getAttribute(MailAttribute);
if (emailAttr == null)
{
throw new Exception("Your account is missing an email.");
}
var memberAttr = user.getAttribute(MemberOfAttribute);
if (memberAttr == null)
{
throw new Exception("Your account is missing roles.");
}
return new AppUser
{
DisplayName = displayNameAttr.StringValue,
Username = accountNameAttr.StringValue,
Email = emailAttr.StringValue,
Roles = memberAttr.StringValueArray
.Select(x => GetGroup(x))
.Where(x => x != null)
.Distinct()
.ToArray()
};
}
}
}
finally
{
_connection.Disconnect();
}
return null;
}
}
}
AccountController.cs
Then finally after the user is verified, you need to construct the principal from the user claims for sign in process, which would generate the cookie behind the scene.
public class AccountController : Controller
{
private readonly IAuthenticationService _authService;
public AccountController(IAuthenticationService authService)
{
_authService = authService;
}
...
[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> Login(LoginViewModel model)
{
if (ModelState.Valid)
{
try
{
var user = _authService.Login(model.Username, model.Password);
if (user != null)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, user.Username),
new Claim(CustomClaimTypes.DisplayName, user.DisplayName),
new Claim(ClaimTypes.Email, user.Email)
}
// Roles
foreach (var role in user.Roles)
{
claims.Add(new Claim(ClaimTypes.Role, role));
}
// Construct Principal
var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, _authService.GetType().Name));
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme,
principal,
new AuthenticationProperties
{
IsPersistent = model.RememberMe
}
);
return Redirect(Url.IsLocalUrl(model.ReturnUrl)
? model.ReturnUrl
: "/");
}
ModelState.AddModelError("", #"Your username or password is incorrect.");
}
catch(Exception ex)
{
ModelState.AddModelError("", ex.Message);
}
}
return View(model);
}
}
Would this post help you integrate with AD for Authentication and Authorization?
MVC Core How to force / set global authorization for all actions?
The idea is add authentication within ConfigureServices method in Startup.cs file:
services.AddMvc(config =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.RequireRole([Your AD security group name in here without domain name]) // This line adds authorization to users in the AD group only
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
});
In Asp.Net Core the Authentication is controlled through project properties.
Open the solution. Right click on the Project and Click Properties.
Click the Debug tab. Check the Enable Windows Authentication checkbox. Ensure Anonymous Authentication is disabled.
Here is Microsoft's document, https://learn.microsoft.com/en-us/aspnet/core/security/authentication/windowsauth
Cheers!

WebAPI : How to add the Account / Authentication logic to a self hosted WebAPI service

I just came across a great reference example of using authenticated WebAPI with AngularJS:
http://www.codeproject.com/Articles/742532/Using-Web-API-Individual-User-Account-plus-CORS-En?msg=4841205#xx4841205xx
An ideal solution for me would be to have such WebAPI service self hosted instead of running it as a Web application.
I just do not know where to place all of the authentication / authorization logic within a self hosted (OWIN / Topshelf) solution.
For example, in the Web app, we have these two files: Startup.Auth, and ApplicationOAuthProvider:
Startup.Auth:
public partial class Startup
{
static Startup()
{
PublicClientId = "self";
UserManagerFactory = () => new UserManager<IdentityUser>(new UserStore<IdentityUser>());
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/Token"),
Provider = new ApplicationOAuthProvider(PublicClientId, UserManagerFactory),
AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
AllowInsecureHttp = true
};
}
public static OAuthAuthorizationServerOptions OAuthOptions { get; private set; }
public static Func<UserManager<IdentityUser>> UserManagerFactory { get; set; }
public static string PublicClientId { get; private set; }
// For more information on configuring authentication, please visit http://go.microsoft.com/fwlink/?LinkId=301864
public void ConfigureAuth(IAppBuilder app)
{
// Enable the application to use a cookie to store information for the signed in user
// and to use a cookie to temporarily store information about a user logging in with a third party login provider
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
// Enable the application to use bearer tokens to authenticate users
app.UseOAuthBearerTokens(OAuthOptions);
}
}
ApplicationOAuthProvider:
public class ApplicationOAuthProvider : OAuthAuthorizationServerProvider
{
private readonly string _publicClientId;
private readonly Func<UserManager<IdentityUser>> _userManagerFactory;
public ApplicationOAuthProvider(string publicClientId, Func<UserManager<IdentityUser>> userManagerFactory)
{
if (publicClientId == null)
{
throw new ArgumentNullException("publicClientId");
}
if (userManagerFactory == null)
{
throw new ArgumentNullException("userManagerFactory");
}
_publicClientId = publicClientId;
_userManagerFactory = userManagerFactory;
}
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
// Add Access-Control-Allow-Origin header as Enabling the Web API CORS will not enable it for this provider request.
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });
using (UserManager<IdentityUser> userManager = _userManagerFactory())
{
IdentityUser user = await userManager.FindAsync(context.UserName, context.Password);
if (user == null)
{
context.SetError("invalid_grant", "The user name or password is incorrect.");
return;
}
ClaimsIdentity oAuthIdentity = await userManager.CreateIdentityAsync(user,
context.Options.AuthenticationType);
ClaimsIdentity cookiesIdentity = await userManager.CreateIdentityAsync(user,
CookieAuthenticationDefaults.AuthenticationType);
AuthenticationProperties properties = CreateProperties(user.UserName);
AuthenticationTicket ticket = new AuthenticationTicket(oAuthIdentity, properties);
context.Validated(ticket);
context.Request.Context.Authentication.SignIn(cookiesIdentity);
}
}
public override Task TokenEndpoint(OAuthTokenEndpointContext context)
{
foreach (KeyValuePair<string, string> property in context.Properties.Dictionary)
{
context.AdditionalResponseParameters.Add(property.Key, property.Value);
}
return Task.FromResult<object>(null);
}
public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
// Resource owner password credentials does not provide a client ID.
if (context.ClientId == null)
{
context.Validated();
}
return Task.FromResult<object>(null);
}
public override Task ValidateClientRedirectUri(OAuthValidateClientRedirectUriContext context)
{
if (context.ClientId == _publicClientId)
{
Uri expectedRootUri = new Uri(context.Request.Uri, "/");
if (expectedRootUri.AbsoluteUri == context.RedirectUri)
{
context.Validated();
}
}
return Task.FromResult<object>(null);
}
public static AuthenticationProperties CreateProperties(string userName)
{
IDictionary<string, string> data = new Dictionary<string, string>
{
{ "userName", userName }
};
return new AuthenticationProperties(data);
}
}
I'm looking for a way to integrate these into my OWIN self hosted app, and have these authentication features. start upon application startup, and function as they do in the Web app version.