c# web api using auth1.0a with OAuthAuthorizationServerProvider - c#

I set up JWT token based authentication using the OAuthAuthorizationServerProvider a while ago. The provider looks like this:
public class OAuthProvider : OAuthAuthorizationServerProvider
{
// Private properties
private readonly IAdvancedEncryptionStandardProvider _helper;
private readonly IUserProvider _userProvider;
// Optional fields
private readonly Lazy<IClientService> _clientService;
/// <summary>
/// Default constructor
/// </summary>
/// <param name="helper"></param>
public OAuthProvider(IAdvancedEncryptionStandardProvider helper, IUserProvider userProvider, Lazy<IClientService> clientService)
{
_helper = helper;
_userProvider = userProvider;
_clientService = clientService;
}
/// <summary>
/// Always validate the client because we are using angular
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
// Set up our variables
var clientId = string.Empty;
var clientSecret = string.Empty;
Client client = null;
// Try to get our credentials if basic authentication has been used
if (!context.TryGetBasicCredentials(out clientId, out clientSecret))
context.TryGetFormCredentials(out clientId, out clientSecret);
// If we have no client id
if (context.ClientId == null)
{
//Remove the comments from the below line context.SetError, and invalidate context
//if you want to force sending clientId/secrects once obtain access tokens.
context.Validated();
//context.SetError("invalid_clientId", "ClientId should be sent.");
return;
}
// Get our client
client = await _clientService.Value.GetAsync(context.ClientId);
// If we have no client, throw an error
if (client == null)
{
context.SetError("invalid_clientId", $"Client '{ context.ClientId }' is not registered in the system.");
return;
}
// Get the application type
if (client.ApplicationType == ApplicationTypes.NativeConfidential)
{
// If we have a client secret
if (string.IsNullOrWhiteSpace(clientSecret))
{
context.SetError("invalid_clientId", "Client secret shoud be sent.");
return;
}
if (client.Secret != _helper.Encrypt(clientSecret))
{
context.SetError("invalid_clientId", "Client secret is invalid.");
return;
}
}
// If the client is inactive, throw an error
if (!client.Active)
{
context.SetError("invalid_clientId", "Client is inactive.");
return;
}
// Set our allowed origin and token expiration
context.OwinContext.Set<string>("as:clientAllowedOrigin", client.AllowedOrigin);
context.OwinContext.Set<string>("as:clientRefreshTokenLifeTime", client.RefreshTokenLifeTime.ToString());
// Validate our request
context.Validated();
return;
}
/// <summary>
/// Authorize the request
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
// Set our allowed origin
var allowedOrigin = context.OwinContext.Get<string>("as:clientAllowedOrigin");
if (string.IsNullOrEmpty(allowedOrigin))
allowedOrigin = "*";
// Add our CORS
context.OwinContext.Response.Headers.Remove("Access-Control-Allow-Origin");
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { allowedOrigin });
// Find user by username first
var user = await _userProvider.FindByNameAsync(context.UserName);
// If our user actually exists
if (user != null)
{
// Validate the users credentials
var validCredentials = await _userProvider.FindAsync(context.UserName, context.Password);
var lockoutEnabled = await _userProvider.GetLockoutEnabledAsync(user.Id);
// If lockout is enabled
if (lockoutEnabled)
{
// If the user entered invalid credentials
if (validCredentials == null)
{
// Record the failure which also may cause the user to be locked out
await _userProvider.AccessFailedAsync(user);
// Find out how many attempts are left
var accessFailedCount = await _userProvider.GetAccessFailedCountAsync(user.Id);
var attemptsLeft = Convert.ToInt32(ConfigurationManager.AppSettings["MaxFailedAccessAttemptsBeforeLockout"].ToString()) - accessFailedCount;
// Inform the user of the error
context.SetError("invalid_grant", string.Format(Resources.PasswordInvalid, attemptsLeft));
return;
}
// Check to see if the user is already locked out
var lockedOut = await _userProvider.IsLockedOutAsync(user.Id);
// If the user is lockted out
if (lockedOut)
{
// Inform the user
context.SetError("invalid_grant", string.Format(Resources.UserLocked, ConfigurationManager.AppSettings["DefaultAccountLockoutTimeSpan"].ToString()));
return;
}
// If we get this far, reset the access attempts
await _userProvider.ResetAccessFailedCountAsync(validCredentials);
}
// If the user entered the correct details
if (validCredentials != null)
{
// If the user has not confirmed their account
if (!validCredentials.EmailConfirmed)
{
// Inform the user
context.SetError("invalid_grant", Resources.UserHasNotConfirmed);
return;
}
// Generate our identity
var oAuthIdentity = await _userProvider.CreateIdentityAsync(validCredentials, "JWT");
oAuthIdentity.AddClaims(ExtendedClaimsProvider.GetClaims(validCredentials));
// Create our properties
var properties = new AuthenticationProperties(new Dictionary<string, string>
{
{"as:client_id", string.IsNullOrEmpty(context.ClientId) ? string.Empty : context.ClientId},
{"userName", context.UserName}
});
// Create our ticket and authenticate the user
var ticket = new AuthenticationTicket(oAuthIdentity, properties);
context.Validated(ticket);
return;
}
}
// Failsafe
context.SetError("invalid_grant", Resources.UserOrPasswordNotFound);
return;
}
/// <summary>
/// Adds additional properties to the response
/// </summary>
/// <param name="context">The current context</param>
/// <returns></returns>
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);
}
/// <summary>
/// Grants a refresh token for the current context
/// </summary>
/// <param name="context">The current context</param>
/// <returns></returns>
public override Task GrantRefreshToken(OAuthGrantRefreshTokenContext context)
{
// Get our client ids
var originalClient = context.Ticket.Properties.Dictionary["as:client_id"];
var currentClient = context.ClientId;
// If we are not the same client
if (originalClient != currentClient)
{
// Set the error and exit the function
context.SetError("invalid_clientId", "Refresh token is issued to a different clientId.");
return Task.FromResult<object>(null);
}
// Change auth ticket for refresh token requests
var newIdentity = new ClaimsIdentity(context.Ticket.Identity);
newIdentity.AddClaim(new Claim("newClaim", "newValue"));
var newTicket = new AuthenticationTicket(newIdentity, context.Ticket.Properties);
context.Validated(newTicket);
return Task.FromResult<object>(null);
}
}
This has worked well and for the most part is fine. My manager how now asked me to put authentication in for other applications using a key and secret.
I would like to use the OAuthAuthorizationServerProvider to do this, but I cannot find any documentation anywhere of how to go about setting this up.
I have read and found a method which can be override: GrantCustomExtension and thought that maybe I could use this to set up the authentication but like I have mentioned, I have no idea how to set it up.
Has anyone had experience with this? If they have, could they help me by providing a code example or giving me a link to a resource that I can read?
Any help would be greatly appreciated.

I recommend to step away from Asp.net Identity and use IdentityServer.
IdentityServer4 is an OpenID Connect and OAuth 2.0 framework for ASP.NET Core.
and IdentityServer3 is for Asp.Net Classic ( although you can use IdentityServer4 with Asp.net classic )
it's really easy to config and very ongoing project.
it has several features like
Authentication as a Service
Single Sign-on / Sign-out
Access Control for APIs
Federation Gateway
and for the most important part, it's free and open source.

Related

OpenIddict migration from v2 to v3 rc1 - Login success, Authorize Failed

I have tried to migration my project from v2 to v3 and I think i have updated everything, but for some reason i can login fine and i get an access_token returned, but this access_token fails to authorize a request. Not sure why, but if someone can see anything obvious that would be great. My application is a web Api with a angular2 web application for the client.
In v2 i was using JWT, but i understand that this is the default in v3 so i may have some code that needs to removed for this to work.
To restrict a controllers methods or a single method, i am using [Authorize] attribute.
Extension method to be called in Startup.cs for adding Authentication, done to keep Startup.cs file from getting to big and keeps things in one place
/// <summary>
/// Add authentication
/// </summary>
/// <param name="services"></param>
/// <param name="appSettings"></param>
public static void AddAuthentication(this IServiceCollection services, AppSettings appSettings)
{
// appSettings.ApiUrl = "http://localhost:5000"
// appSettings.WebsiteUrl = "http://localhost:4200"
// the default value for AllowuserNameCharacters is "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._#+"
// here we have just added some additional characters
services.AddIdentity<User, Role>(options => { options.User.AllowedUserNameCharacters += "'&"; })
.AddDefaultTokenProviders();
// Configure Identity to use the same JWT claims as OpenIddict instead
// of the legacy WS-Federation claims it uses by default (ClaimTypes),
// which saves you from doing the mapping in your authorization controller.
services.Configure<IdentityOptions>(options =>
{
options.ClaimsIdentity.UserNameClaimType = Claims.Name;
options.ClaimsIdentity.UserIdClaimType = Claims.Subject;
options.ClaimsIdentity.RoleClaimType = Claims.Role;
});
// return unauthorized message instead of url
services.ConfigureApplicationCookie(options =>
{
options.Events.OnRedirectToLogin = context =>
{
context.Response.StatusCode = 401;
return System.Threading.Tasks.Task.CompletedTask;
};
});
// configure all tokens generated from aspnet to expire in 3 days (create password, forget password etc)
services.Configure<DataProtectionTokenProviderOptions>(options => options.TokenLifespan = TimeSpan.FromDays(3));
// Authentication
var authenticationBuilder = services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(cfg =>
{
cfg.Authority = appSettings.ApiUrl;
cfg.Audience = appSettings.ApiUrl;
cfg.RequireHttpsMetadata = false;
cfg.SaveToken = true;
});
// OpenIddict
services.AddOpenIddict()
.AddCore(options =>
{
options.UseEntityFramework(e => e.UseDbContext<ApplicationDbContext>());
})
.AddServer(options =>
{
// For token lifetimes look at the below link
// https://github.com/openiddict/openiddict-core/wiki/Configuration-and-options
// Enable the authorization, logout, token endpoints.
options.SetAuthorizationEndpointUris("/connect/authorize")
.SetLogoutEndpointUris("/connect/logout")
.SetTokenEndpointUris("/connect/token");
// Note: the Mvc.Client sample only uses the code flow and the password flow, but you
// can enable the other flows if you need to support implicit or client credentials.
options.AllowPasswordFlow()
.AllowRefreshTokenFlow();
// Mark the "email", "profile" and "roles" scopes as supported scopes.
options.RegisterScopes(Scopes.Email,
Scopes.Profile,
Scopes.OpenId,
Scopes.OfflineAccess,
Scopes.Roles);
// code to allow requests without client_id
options.AcceptAnonymousClients();
options.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate();
// Register the ASP.NET Core MVC binder used by OpenIddict.
// Note: if you don't call this method, you won't be able to
// bind OpenIdConnectRequest or OpenIdConnectResponse parameters.
options.UseAspNetCore()
.EnableAuthorizationEndpointPassthrough()
.EnableLogoutEndpointPassthrough()
.EnableTokenEndpointPassthrough()
.DisableTransportSecurityRequirement(); // Never use https because we use load balancer
})
.AddValidation(options =>
{
// Import the configuration from the local OpenIddict server instance.
options.UseLocalServer();
// Register the ASP.NET Core host.
options.UseAspNetCore();
});
}
Startup.cs
/// <summary>
/// Configure services
/// </summary>
/// <param name="services"></param>
public void ConfigureServices(IServiceCollection services)
{
// application settings
var appSettings = new AppSettings();
Configuration.Bind(appSettings);
// mvc
services.AddMvc(appSettings);
// identity / saml etc
services.AddAuthentication(appSettings);
}
/// <summary>
/// Configure application
/// </summary>
/// <param name="applicationBuilder"></param>
/// <param name="webHostEnvironment"></param>
/// <param name="loggerFactory"></param>
public void Configure(IApplicationBuilder applicationBuilder, IWebHostEnvironment webHostEnvironment, ILoggerFactory loggerFactory)
{
// get current appSettings and output to logger
var appSettings = applicationBuilder.ApplicationServices.GetRequiredService<AppSettings>();
var logger = loggerFactory.CreateLogger(appSettings.ApiUrl);
logger.LogInformation(string.Format("{0}\n{1}", appSettings.Environment, appSettings.Data.DefaultConnection.ConnectionString));
// environment
webHostEnvironment.EnvironmentName = appSettings.Environment;
// CORS
applicationBuilder.UseCors("Default");
// static files in root
applicationBuilder.UseStaticFiles();
// routing
applicationBuilder.UseRouting();
// comment this out and you get an error saying
// InvalidOperationException: No authentication handler is configured to handle the scheme: Microsoft.AspNet.Identity.External
applicationBuilder.UseAuthentication();
// for authorization headers
applicationBuilder.UseAuthorization();
// response caching
applicationBuilder.UseResponseCaching();
// routes
applicationBuilder.UseEndpoints(options =>
{
// default goes to Home, and angular will deal with client side routing
options.MapControllerRoute(
name: "default",
pattern: "{*url}",
defaults: new { controller = "home", action = "index" });
});
}
I have updated my AuthorizationController.cs as well
/// <summary>
/// Handles authorization requests
/// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public class AuthorizationController : Controller
{
private IOptions<IdentityOptions> _identityOptions;
private OpenIddictApplicationManager<OpenIddictEntityFrameworkApplication> _applicationManager;
private SignInManager<MyUser> _signInManager;
private UserManager<MyUser> _userManager;
private AppSettings _appSettings;
private IEncryptionService _encryptionService;
/// <summary>
/// Create a new authorization controller
/// </summary>
/// <param name="identityOptions"></param>
/// <param name="applicationManager"></param>
/// <param name="signInManager"></param>
/// <param name="userManager"></param>
/// <param name="appSettings"></param>
/// <param name="encryptionService"></param>
public AuthorizationController(
IOptions<IdentityOptions> identityOptions,
OpenIddictApplicationManager<OpenIddictEntityFrameworkApplication> applicationManager,
SignInManager<MyUser> signInManager, UserManager<MyUser> userManager,
AppSettings appSettings, IEncryptionService encryptionService)
{
_identityOptions = identityOptions;
_applicationManager = applicationManager;
_signInManager = signInManager;
_userManager = userManager;
_appSettings = appSettings;
_encryptionService = encryptionService;
}
// Note: to support interactive flows like the code flow,
// you must provide your own authorization endpoint action:
/// <summary>
/// Authorize an openId request
/// </summary>
/// <param name="connectRequest"></param>
/// <returns></returns>
[Authorize, HttpGet, Route("~/connect/authorize")]
public async Task<IActionResult> Authorize()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
// Retrieve the application details from the database.
var application = await _applicationManager.FindByClientIdAsync(request.ClientId, new System.Threading.CancellationToken());
if (application == null)
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(
new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidClient,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "Details concerning the calling client application cannot be found in the database"
}
)
);
}
// Flow the request_id to allow OpenIddict to restore
// the original authorization request from the cache.
return View(new AuthorizeViewModel
{
ApplicationName = application.DisplayName,
RequestId = request.RequestId,
Scope = request.Scope
});
}
/// <summary>
/// Accept an openId request
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
[Authorize, HttpPost("~/connect/authorize/accept"), ValidateAntiForgeryToken]
public async Task<IActionResult> Accept()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
// Retrieve the profile of the logged in user
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(
new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ServerError,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "An internal error has occurred"
}
)
);
}
// create a new principal
var principal = await CreatePrincipalAsync(request, user);
// returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
/// <summary>
/// Deny an openId request
/// </summary>
/// <returns></returns>
[Authorize, HttpPost("~/connect/authorize/deny"), ValidateAntiForgeryToken]
public IActionResult Deny()
{
// Notify OpenIddict that the authorization grant has been denied by the resource owner
// to redirect the user agent to the client application using the appropriate response_mode.
return Forbid(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
/// <summary>
/// Logout
/// </summary>
/// <returns></returns>
[HttpPost("~/connect/logout")]
public async Task<IActionResult> Logout()
{
// Ask ASP.NET Core Identity to delete the local and external cookies created
// when the user agent is redirected from the external identity provider
// after a successful authentication flow (e.g Google or Facebook).
await _signInManager.SignOutAsync();
// Returning a SignOutResult will ask OpenIddict to redirect the user agent
// to the post_logout_redirect_uri specified by the client application.
return SignOut(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
// Note: to support non-interactive flows like password,
// you must provide your own token endpoint action:
/// <summary>
/// Exchange request for valid openId token
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
[HttpPost("~/connect/token")]
[Produces("application/json")]
public async Task<IActionResult> Exchange()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
if (request.IsPasswordGrantType())
{
var user = await _userManager.FindByNameAsync(request.Username);
if (user == null)
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(
new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The username/password couple is invalid"
}
)
);
}
// Ensure the password is valid.
if (!await _userManager.CheckPasswordAsync(user, request.Password))
{
if (_userManager.SupportsUserLockout)
{
await _userManager.AccessFailedAsync(user);
}
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(
new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The username/password couple is invalid"
}
)
);
}
if (_userManager.SupportsUserLockout)
{
await _userManager.ResetAccessFailedCountAsync(user);
}
// create a new principal
var principal = await CreatePrincipalAsync(request, user);
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
else if (request.IsRefreshTokenGrantType())
{
// Retrieve the claims principal stored in the authorization code/refresh token.
var info = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
// Retrieve the user profile corresponding to the refresh token
var user = await _userManager.GetUserAsync(info.Principal);
if (user == null)
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(
new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The refresh token is no longer valid"
}
)
);
}
// Ensure the user is still allowed to sign in
if (!await _signInManager.CanSignInAsync(user))
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(
new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is no longer allowed to sign in"
}
)
);
}
// create a new principal
var principal = await CreatePrincipalAsync(request, user);
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
throw new NotImplementedException("The specified grant type is not implemented");
}
/// <summary>
/// Creates a principal based on the openId request
/// </summary>
/// <param name="request"></param>
/// <param name="user"></param>
/// <param name="properties"></param>
/// <returns></returns>
private async Task<ClaimsPrincipal> CreatePrincipalAsync(OpenIddictRequest request, MyUser user, AuthenticationProperties properties = null)
{
// Create a new ClaimsPrincipal containing the claims that
// will be used to create an id_token, a token or a code.
var principal = await _signInManager.CreateUserPrincipalAsync(user);
if (!request.IsRefreshTokenGrantType())
{
// Set the list of scopes granted to the client application.
// Note: the offline_access scope must be granted
// to allow OpenIddict to return a refresh token.
principal.SetScopes(new[]
{
Scopes.OpenId,
Scopes.Email,
Scopes.Profile,
Scopes.OfflineAccess,
Scopes.Roles
}.Intersect(request.GetScopes()));
}
// Set resource
principal.SetResources(new string[] { _appSettings.ApiUrl });
// Note: by default, claims are NOT automatically included in the access and identity tokens.
// To allow OpenIddict to serialize them, you must attach them a destination, that specifies
// whether they should be included in access tokens, in identity tokens or in both.
foreach (var claim in principal.Claims)
{
claim.SetDestinations(GetDestinations(claim, principal));
}
return principal;
}
private IEnumerable<string> GetDestinations(Claim claim, ClaimsPrincipal principal)
{
// Note: by default, claims are NOT automatically included in the access and identity tokens.
// To allow OpenIddict to serialize them, you must attach them a destination, that specifies
// whether they should be included in access tokens, in identity tokens or in both.
switch (claim.Type)
{
case Claims.Name:
yield return Destinations.AccessToken;
if (principal.HasScope(Scopes.Profile))
yield return Destinations.IdentityToken;
yield break;
case Claims.Email:
yield return Destinations.AccessToken;
if (principal.HasScope(Scopes.Email))
yield return Destinations.IdentityToken;
yield break;
case Claims.Role:
yield return Destinations.AccessToken;
if (principal.HasScope(Scopes.Roles))
yield return Destinations.IdentityToken;
yield break;
// Never include the security stamp in the access and identity tokens, as it's a secret value.
case "AspNet.Identity.SecurityStamp": yield break;
default:
yield return Destinations.AccessToken;
yield break;
}
}
}
Also here is my applicationDbContext
/// <summary>
/// Application context to hold our openIddict entities
/// </summary>
public class ApplicationDbContext : DbContext, IApplicationDbContext
{
/// <summary>
/// Create context with connection string
/// </summary>
/// <param name="connectionString"></param>
public ApplicationDbContext(string connectionString) : base(connectionString)
{
}
/// <summary>
/// When creating models
/// </summary>
/// <param name="modelBuilder"></param>
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.UseOpenIddict();
}
/// <summary>
/// Authorizations
/// </summary>
public virtual DbSet<OpenIddictEntityFrameworkAuthorization> Authorization { get; set; }
/// <summary>
/// Applications
/// </summary>
public virtual DbSet<OpenIddictEntityFrameworkApplication> Application { get; set; }
/// <summary>
/// Tokens
/// </summary>
public virtual DbSet<OpenIddictEntityFrameworkToken> Token { get; set; }
/// <summary>
/// Scopes
/// </summary>
public virtual DbSet<OpenIddictEntityFrameworkScope> Scope { get; set; }
}
As i said, i am getting a token returned, like below
{
"access_token": "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZDQkMtSFM1MTIiLCJraWQiOiI5NDVDNjFERjcyRUU1OUNDNUNFQzQ0NzJCRDE0MDg4NDEyNTAyRjM3IiwidHlwIjoiYXQrand0In0.TxmA-Lv60Lwvr5eXdu2fgG95Bx4AUoPvp5XniqSYbBgmpDrVXCUpSjnEv7tWQ5QLPjks6TAy8hGAGfgQPFB1xDM05mZyu2WDPBsoEQ2VtxuabaPhj-uK2KjnWEccjS-n-YAY7OlSDFXrmiDsFCOf3jaPGZ43gUCPyTVl_WVE_KBGVXa19RDZFf9Ger-TsHY3evYbsUchOKMco-CD2IGt6Xg6DttxFcKF3SUPwMXhn6AGdkNesGVIoVSeDd_CnM3MyHtJIbGbkRkmwpMWsbwaWGI38itUJ0XeUqn45B_GsZfaZllytuMHbuqt1GgL0gdOaCfBUxaqy7AaYsY9UKlaEw.LS4FnIOCRESdeo1c5K2_5Q.Ic8fssDScEu7lAomL8d_7JZMVho5hLphg269UIETZui7DUB2gund7YCtGq7imdnDtN-wsEoZaLHJ4UQSJrpuIMCN2pW69J9kKcx-UT1e2Ma-JNj2G5CtxwRf_bszRRsgWC1ia85LU5TPsUFvhd_wDxyUax2GKowb0FVl8EFRoFdZF40h06LaofxDzD2BFdwSYaHaDm3icVNZ0CRpYCoZq-MK_c3Fl98l57zjZl1CNscs1w0trApMDvQ7UeFcez3zelN24H6TCaXqTRP3lixMiv9Rtm3Kqkv67HEUAFD-vXIMyWQTo28oJMMAQz9zQeTp88JsfI4Pv7euUECEkwK7Fe5rbxhD4oicNNa_wmRo2vsjrpm4C7mmRKH2u3cX2CCTwahOsVHxu4kO3zliWmniW-krEAAsaW6BjoWqJiGN3ydoiHLIv3muM78jt85KzKxMIFsalpdf2F9Z8neQBglhzxZQFp9JiEecczxao6Y03WuFNX98kZh8Zr4D21CM_m634u3mf5-Gz-frVlSgnMrE9vgCG2eETI3fOnObf9pQ1XWpK7I9SE0AZvc2PciaKO4H7mKDqXuXFrzdiz6Tx9G9MMkXXbuKsuslZJ6q9wlE3xOl9mzd6shN9GvfSTQNrFoV-PS62xhnfCFKQDfYLzWF1fhQbq_RECp6IVJN960jaMAGIZ0yzIbRInIkbhdFEIbl__h9AYo6FfcZgYv4tfw-iB_unezgHCIPpbBPC0eRMiZh1wqpfwpiJ8zjvcXJXXCfSlu13KlBWD9tHVEHh6aHE258hGdFzApA-MTCkjHwMRjVgmv_-Ed-xwk0hjBpRhiZv-0kNwKI72YJVgZMX9CMpFBg2CO5z5hHSlc0pO9pc5ovuCZ_dYYRiqqcIDUJ5Wl6dDt1JCPI084C4yssC7MC4e94OtvfRtrI096qMI34qrLWi-jn7UOMnWMUunNepQLwQ07DK-ubXsS-m0xvSxPYxtE6XM6QebmvCcXj__vELGscDu8mWmvP1Y0L9SoSpWlErHvlCPpkfVeMnkdP74cKTYgpXSQcGduqcdfU86leI9oUYgnrcPwvMgUq3jwgHGWn_0d4Bo8CsChWatYWmSNKN88h3WfASSl6SyqeVbSSYvIp-0HBGxGOwODgO-YsOlOgeKmm8oSIhnELVNMVEY1uiUZpJ9DGfrUUXKMw7aIz6LK_zH_HxlMiBam0fxgAQwRyYlO0AFmuxFO62KEHFEjdDSEO9pcUP03_RNqDfAX-IAV_EoFT7CwVpOZMUvFLo78S4xkq2ss3CFbkA3J4ioud88T5SUfslnsZmY1dJYtW4HhlGF7SKVMN6GSwckz1YhyaxqlQpMbMRFA0uCbkM6u41K0-_toRQejKDX5juqFwqK8.m0bmPYOuAYYdc6WfjDWP2UneysP1G3FqwlmOzWOrTnM",
"token_type": "Bearer",
"expires_in": 3599,
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjcwMzJBRUJBQjc4NkNCM0ExRkI3Nzk1NjdDQTRGRjU1ODg3OTFCNDEiLCJ4NXQiOiJjREt1dXJlR3l6b2Z0M2xXZktUX1ZZaDVHMEUiLCJ0eXAiOiJKV1QifQ.eyJzdWIiOiIxIiwibmFtZSI6InBvd2VydXNlckBtZWFzdXJlMmltcHJvdmUuY29tIiwib2lfYXVfaWQiOiIxMDJjZTE0NC02NzkyLTRlYzAtOTlhNy0xZDBlMzdmNzkyZTYiLCJhdF9oYXNoIjoiV3BibExxNFdNR2dwdHdGVVVSMktRdyIsIm9pX3Rrbl9pZCI6IjQyMzBlYmJkLWUyNzItNDJlZS1iOTZiLTQ1ZGJmMTJlZWJmMiIsImV4cCI6MTYwNjQ3NzgzOSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo1MDAwLyIsImlhdCI6MTYwNjQ3NjYzOX0.WZkB63ZfZuJfigNVabPegu-B8TvMeS1DmgRQJS151XGR08Pw-fcCldb35oM7ZW9oQenj7059BAZMI1EveHWNVWOEFpabebi7TccGRoR1YKqWSNWTBDwyQgGMyehVmze_TPgsSjAJA0y1f_xtF3-ImfVx5Tzlcjg4XAmAhV3MRd-fEobdGk5540uto5hZJ7ieHrV_7U_FF4NgVT5nSw92bkFNjUokmNgMBpDWelZUEXmsb3MFGDMnQkP0oTGXeIcy0nuuIKpr1Liza_cvv1JfICQnSUKw_u3zdqSbsbXtzGg9GfunEhXf1zSF5dxfbNpr2E4bsnIxGJ-mOYxBsOQDJA",
"refresh_token": "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZDQkMtSFM1MTIiLCJraWQiOiI5NDVDNjFERjcyRUU1OUNDNUNFQzQ0NzJCRDE0MDg4NDEyNTAyRjM3IiwidHlwIjoib2lfcmVmdCtqd3QifQ.E5xUDcqMK15spCgqoM8YrOCn_32wBX_X_XsG_f1nKOR-TcDI2AKi07vX62uMmbO1bgclGSGNynEUTuPt7KATf9UHXwpytXAK3_BnLxQLz7NbYIlfrja9t5z3gCRIGOZ-5gHbHcU6RdJrDpC73_V7CXYIfkVhfWeeS30_3GtLa-BUu4Nlr9YL7K22KdR0LJQsK6SXLMKobsj6PLrrywHnClaw6dG_O_SDezC5M_eIo_ErdHFqUBwWYhUHyntijJ9ekH4A2SiY0LGaQD6QyvmUNPC7E-LtLVO78saJYAgGW-CkQbwTBF7b_vOoEr_mPrFnpytwLnIvxSQCFVjclH-vzA.RGNxh09nh1RWb6XU6jNZSg.VRwdO_5UrB7tqaMHXwt6qZN8VucSlNXnBNoBxcvisDvjWhBOlhrJxEidGQPBeiIpbrYrWQXyPfSgFrmZAlqYcF-3cXTl0W-xR4oW1ZqRf95eMs-YTA9-bR6n8P2pV7WeYky7wkwa-i3Vs9rzasYjK18LTU6v71xMNVN2K0Z2HRgTXxPg3U4HUgkVQpZp4qetyhozdOhGBV37igAHSZHFUSYg-dlstz3DtEX7IXvi7GQbM_fLKVlDLWYZOtX1yH366C9TIjZbxGP3h-qPqfKKDNP-IhD_x2IpymmiKllfhfLKb62JQzy_ci-ICXGvs0z_ZLZirmzVSEotqrcKmwe1rlqRXbymP8llIXe3AzjLonN3I_35bLeqZz1KQE93pkLOpJGAJuhJ4dc8a7wJ5kxTAs9_CCvb6UBvhRxInUyZ9PQq026RaYJvN9i--x8JxAAtH58h9Y5zgm_M4kFbEoGgZshyGZ8QSQrp4JpPrGeW9ElvkyEmrGvEpm6zmy_Y6tEF3lXFfrGJL6FPGD5_q8m9lGQ162OCmZMGg2Sy46Hne0SPv04TpM43F-m82BTzXxGRbpvOoNye7Pl9dKbdOBno1WEmaesL-R-W_8qogNszW5c-gbhQTUjcQ1J5yuVL4r29qN9xhVDPKqxPYPzInCr30m8SH41NI6WusFVJbNoqVs43wymGfQwuDfJacVCIfuT1Lx2VMdVl3nvCAGOnEwiUDufjqMaA-Nh-Row4QIwTHMGwfzxbFkQjRfwq_NjVU0LZlVDX2BgSETw2ak3KpgAGoeDlVVwmXuQpbvZ3wRHVh4a3k3_JVGf1dOiHbXVcWx984-EARKmW6gLjIhe2t5tzIJ9NQqBrdBrCvMmQXWfBiP00-YH31yDfYRF9tNZoojnkcrkOewHFUsEcFuayBzP5ySR6RgGVVE_zkEXTnBXxeiynzfJn0D3XthpRPosLJTJz91tdmvo6CxPWuaaZoUlXq_EXqFrGCb09f11Hs_mKu7T7pHMbJioTAvj1Jy8jzduMU3Rth-w0A8Md2gdeKmoCXW51lvpuyFsP4R8AdthhXGUbAk6rNJXdkaUkRyygYU8ZQWI2WCyroDnBRpEIaBtjYT1Uf4zXyTUZ3jicKe5Rjr08tmgAQd_3pNu1Wb2dBQgvcoqnwXcnLvoMTp0yk7Osae94Pqh_Oyz45kz_oS_7fR6WvZm-avBPYLmW92eQWiMG6glxPPe_Vnq9ghwjYjS4KEvyFaDqusDWmxBEDwEzLgBTF3p0R2saedTbwpG4Epcey_T18KEuklAWbCwz-fvV3ip-_wGUNe6cuAOeyuXcUHm18Le0knZt6xNS_a8cxMz9RAEpBs3a-tOWQLrVELXxnNY03NL2szkNQuDUzY9JRtTxqngxcPpHVk058dRG1rwFZqiI4-6_yL03X_fbXsIR0ItBig9surYyB_crwyH3C6OZTnGgwxLKUU9qY0LNTzR0gatrrT1l8NrorLAvODzQqhrqqClHgZkVoQXANvz3mNZWIpxjCkOgDkH-YOaaR3egnwLVMk_clo3-gC76UQ-5T-NnZ7MJTv2twFBhUKHABEJsT9a3nR7ra2CbHFzJFNvRRCPHAGOVAY-y-Ek0xZn8mvd3Fvw4wlej4EdQvlnFUcaIH4Cl0MZD9G9t2W3A96KLRZctVfGK6W1yM846DWHwSfeTj3ZPN_6bYDqbWHoXrQxe5BGi9aN8PXfL1Bu9W5jhQpgrvTRvnjhYqwN24ta9r6BbrMzBL3cszf56dG3Ko6aA8gDwUmDSjyNVhk76sdoW.1xLMuQf7NjeYjAXDL5BUxaoFSR0EECEzPh7XxkE8Hj4"
}
Here is the token being passed successfully, but Unauthorized is returned, as you can see the next call fails as well.
Any help much appreciated
I know this is old, but just post my solution in case anyone has the same issue.
Just specificy the schema for the Authorize attribute:
[Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)]

Is it possible to access the clientId from an authToken (RequestContext) in WebAPI 2

I have implemented Oauth with my WebAPI 2 application and there are several applications access the API. Once authenticated, after making a request whilst sending across the auth token, I can access the user as follows:
var currentUser = RequestContext.Principal;
When logging in, the clientId was set as follows:
context.OwinContext.Set<AppClient>("oauth:client", client);
Is there a way to access that client Id? I want to restrict certain actions / controllers to certain clients. Is there a way to do this?
I have tried getting the client as follows:
var client = Request.GetOwinContext().Get<string>("oauth:client");
but this is not working.
When logging in you can set a claim on the identity in the GrantResourceOwnerCredentials
identity.AddClaim(new Claim("oauth:client", client));
That way it is available once the User Principal's Identity has be set.
You can create an extension method to extract it conveniently
public static class GenericIdentityExtensions {
const string ClientIdentifier = "oauth:client";
/// <summary>
/// Set the client id claim
/// </summary>
/// <param name="identity"></param>
/// <returns></returns>
public static bool SetClientId(this IIdentity identity, string clientId) {
if (identity != null) {
var claimsIdentity = identity as ClaimsIdentity;
if (claimsIdentity != null) {
claimsIdentity.AddClaim(new Claim(ClientIdentifier, clientId));
return true;
}
}
return false;
}
/// <summary>
/// Return the client id claim
/// </summary>
/// <param name="identity"></param>
/// <returns></returns>
public static string GetClientId(this IIdentity identity) {
if (identity != null) {
var claimsIdentity = identity as ClaimsIdentity;
if (claimsIdentity != null) {
return claimsIdentity.FindFirstOrEmpty(ClientIdentifier);
}
}
return string.Empty;
}
/// <summary>
/// Retrieves the first claim that is matched by the specified type if it exists, String.Empty otherwise.
/// </summary>
public static string FindFirstOrEmpty(this ClaimsIdentity identity, string claimType) {
var claim = identity.FindFirst(claimType);
return claim == null ? string.Empty : claim.Value;
}
}
So now once you have the the user principal you can extract the client id from the claims.
var currentUser = RequestContext.Principal;
var client = currentUser.Identity.GetClientId();
You can use AuthenticationProperties instead. Same as answered, in GrantResourceOwnerCredentials just implement next stuff while ticket issuing:
var props = new AuthenticationProperties(new Dictionary<string, string>
{
{
"client_id", clientId
},
});
var ticket = new AuthenticationTicket(identity, props);
context.Validated(ticket);
Then, you'll be able to get value via context.Ticket.Properties.Dictionary["client_id"];

ASP.NET Identity Phone Number Token lifespan and SMS limit

I'm building 2 factor registration API using ASP.NET Identity 2.0.
I'd like to give users ability to confirm their phone numer on demand, so even if they didn't confirm they're phone number when registering they always can request new token (making request to my API) that will be send via SMS and enter it on page (also making request to my API).
In method that is responsible for sending token I'm generating token and sending it as shown below:
var token = await UserManager.GeneratePhoneConfirmationTokenAsync(user.Id);
var message = new SmsMessage
{
Id = token,
Recipient = user.PhoneNumber,
Body = string.Format("Your token: {0}", token)
};
await UserManager.SmsService.SendAsync(message);
and inside UserManager:
public virtual async Task<string> GeneratePhoneConfirmationTokenAsync(TKey userId)
{
var number = await GetPhoneNumberAsync(userId);
return await GenerateChangePhoneNumberTokenAsync(userId, number);
}
Each time I call my method I get SMS message that contains token, problem is user can call that metod unlimited number of times and easily can generate costs - each SMS = cost.
I'd like to limit number of requests user can do to that method to one every X minutes.
Also I noticed that when I do multiple requests I get same token, I've tested my method and it looks that this token is valid for 3 minutes, so if I do request in that minutes time window I'll get same token.
Ideally I'd like to have single parameter that would allow me to specify time interval between requests and phone confirmation token lifespan.
I've tried setting token lifespan inside UserManager class using:
appUserManager.UserTokenProvider = new DataProtectorTokenProvider<User,int>(dataProtectionProvider.Create("ASP.NET Identity"))
{
TokenLifespan = new TimeSpan(0,2,0)//2 minutes
};
but this only affects tokens in email confirmation links.
Do I need to add extra field to my user table that will hold token validity date and check it every time I want to generate and send new token or is there easier way?
How can I specify time interval in which ASP.NET Identity will generate same phone number confirmation token?
I'm no expert but i had the same question and found these two threads with a little help from google.
https://forums.asp.net/t/2001843.aspx?Identity+2+0+Two+factor+authentication+using+both+email+and+sms+timeout
https://github.com/aspnet/Identity/issues/465
I'm going to assume you are correct that the default time limit is 3minutes based on the AspNet Identity github discussion.
Hopefully the linked discussions contain the answers you need to configure a new time limit.
Regarding the rate limiting i'm using the following code which is loosely based on this discussions How do I implement rate limiting in an ASP.NET MVC site?
class RateLimitCacheEntry
{
public int RequestsLeft;
public DateTime ExpirationDate;
}
/// <summary>
/// Partially based on
/// https://stackoverflow.com/questions/3082084/how-do-i-implement-rate-limiting-in-an-asp-net-mvc-site
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class RateLimitAttribute : ActionFilterAttribute
{
private static Logger Log = LogManager.GetCurrentClassLogger();
/// <summary>
/// Window to monitor <see cref="RequestCount"/>
/// </summary>
public int Seconds { get; set; }
/// <summary>
/// Maximum amount of requests to allow within the given window of <see cref="Seconds"/>
/// </summary>
public int RequestCount { get; set; }
/// <summary>
/// ctor
/// </summary>
public RateLimitAttribute(int s, int r)
{
Seconds = s;
RequestCount = r;
}
public override void OnActionExecuting(HttpActionContext actionContext)
{
try
{
var clientIP = RequestHelper.GetClientIp(actionContext.Request);
// Using the IP Address here as part of the key but you could modify
// and use the username if you are going to limit only authenticated users
// filterContext.HttpContext.User.Identity.Name
var key = string.Format("{0}-{1}-{2}",
actionContext.ActionDescriptor.ControllerDescriptor.ControllerName,
actionContext.ActionDescriptor.ActionName,
clientIP
);
var allowExecute = false;
var cacheEntry = (RateLimitCacheEntry)HttpRuntime.Cache[key];
if (cacheEntry == null)
{
var expirationDate = DateTime.Now.AddSeconds(Seconds);
HttpRuntime.Cache.Add(key,
new RateLimitCacheEntry
{
ExpirationDate = expirationDate,
RequestsLeft = RequestCount,
},
null,
expirationDate,
Cache.NoSlidingExpiration,
CacheItemPriority.Low,
null);
allowExecute = true;
}
else
{
// Allow and decrement
if (cacheEntry.RequestsLeft > 0)
{
HttpRuntime.Cache.Insert(key,
new RateLimitCacheEntry
{
ExpirationDate = cacheEntry.ExpirationDate,
RequestsLeft = cacheEntry.RequestsLeft - 1,
},
null,
cacheEntry.ExpirationDate,
Cache.NoSlidingExpiration,
CacheItemPriority.Low,
null);
allowExecute = true;
}
}
if (!allowExecute)
{
Log.Error("RateLimited request from " + clientIP + " to " + actionContext.Request.RequestUri);
actionContext.Response
= actionContext.Request.CreateResponse(
(HttpStatusCode)429,
string.Format("You can call this {0} time[s] every {1} seconds", RequestCount, Seconds)
);
}
}
catch(Exception ex)
{
Log.Error(ex, "Error in filter attribute");
throw;
}
}
}
public static class RequestHelper
{
/// <summary>
/// Retrieves the client ip address from request
/// </summary>
public static string GetClientIp(HttpRequestMessage request)
{
if (request.Properties.ContainsKey("MS_HttpContext"))
{
return ((HttpContextWrapper)request.Properties["MS_HttpContext"]).Request.UserHostAddress;
}
if (request.Properties.ContainsKey(RemoteEndpointMessageProperty.Name))
{
RemoteEndpointMessageProperty prop;
prop = (RemoteEndpointMessageProperty)request.Properties[RemoteEndpointMessageProperty.Name];
return prop.Address;
}
return null;
}
}
I've also seen this library recommended a few times:
https://github.com/stefanprodan/WebApiThrottle

Web API 2 /Token return more information

I am using OAuth to generate tokens for my application, specifically JWT. I have this code in my startup class:
private void ConfigureOAuthTokenGeneration(IAppBuilder app)
{
// Configure the db context and user manager to use a single instance per request
app.CreatePerOwinContext(DatabaseContext.Create);
app.CreatePerOwinContext<UserService>(UserService.Create);
app.CreatePerOwinContext<RoleService>(RoleService.Create);
// Plugin the OAuth bearer JSON Web Token tokens generation and Consumption will be here
var OAuthServerOptions = new OAuthAuthorizationServerOptions()
{
//For Dev enviroment only (on production should be AllowInsecureHttp = false)
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/oauth/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
Provider = new OAuthProvider(),
AccessTokenFormat = new CustomJwtFormat("http://localhost:58127")
};
// OAuth 2.0 Bearer Access Token Generation
app.UseOAuthAuthorizationServer(OAuthServerOptions);
}
As you can see I have set a custom OAuthProvider and for the AccessTokenFormat I am using a CustomJwtFormat. The OAuthProvider looks like this:
public class OAuthProvider : OAuthAuthorizationServerProvider
{
/// <summary>
/// Validate client authentication
/// </summary>
/// <param name="context">The current context</param>
/// <returns></returns>
public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
// Validate all requests (because our front end is trusted)
context.Validated();
// Return nothing
return Task.FromResult<object>(null);
}
/// <summary>
/// Validate user credentials
/// </summary>
/// <param name="context">The current context</param>
/// <returns></returns>
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
// Allow any origin
var allowedOrigin = "*";
// Add the access control allow all to our headers
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { allowedOrigin });
// Get our user service
var service = context.OwinContext.GetUserManager<UserService>();
// Find out user
var user = await service.FindAsync(context.UserName, context.Password);
// If the user is not found
if (user == null)
{
// Set an error
context.SetError("invalid_grant", "The user name or password is incorrect.");
// Return from the function
return;
}
// If the user has not confirmed their account
if (!user.EmailConfirmed)
{
// Set an error
context.SetError("invalid_grant", "User did not confirm email.");
// Return from the function
return;
}
// Generate the identity for the user
var oAuthIdentity = await user.GenerateUserIdentityAsync(service, "JWT");
// Create a new ticket
var ticket = new AuthenticationTicket(oAuthIdentity, null);
// Add the ticked to the validated context
context.Validated(ticket);
}
}
which is pretty straight forward. Also, the CustomJwtFormat class looks like this:
/// <summary>
/// JWT Format
/// </summary>
public class CustomJwtFormat : ISecureDataFormat<AuthenticationTicket>
{
// Create our private property
private readonly string issuer;
/// <summary>
/// Default constructor
/// </summary>
/// <param name="issuer">The issuer</param>
public CustomJwtFormat(string issuer)
{
this.issuer = issuer;
}
/// <summary>
/// Method to create our JWT token
/// </summary>
/// <param name="data">The Authentication ticket</param>
/// <returns></returns>
public string Protect(AuthenticationTicket data)
{
// If no data is supplied, throw an exception
if (data == null)
throw new ArgumentNullException("data");
// Get our values from our appSettings
string audienceId = ConfigurationManager.AppSettings["as:AudienceId"];
string symmetricKeyAsBase64 = ConfigurationManager.AppSettings["as:AudienceSecret"];
// Decode our secret and encrypt the bytes
var keyByteArray = TextEncodings.Base64Url.Decode(symmetricKeyAsBase64);
var signingKey = new HmacSigningCredentials(keyByteArray);
// Get our issue and expire dates in UNIX timestamps
var issued = data.Properties.IssuedUtc;
var expires = data.Properties.ExpiresUtc;
// Create our new token
var token = new JwtSecurityToken(this.issuer, audienceId, data.Identity.Claims, issued.Value.UtcDateTime, expires.Value.UtcDateTime, signingKey);
// Create a handler
var handler = new JwtSecurityTokenHandler();
// Write our token string
var jwt = handler.WriteToken(token);
// Return our token string
return jwt;
}
/// <summary>
///
/// </summary>
/// <param name="protectedText"></param>
/// <returns></returns>
public AuthenticationTicket Unprotect(string protectedText)
{
throw new NotImplementedException();
}
}
This is the crucial bit. It uses Thinktecture to generate the token. The line that looks like this:
// Create our new token
var token = new JwtSecurityToken(this.issuer, audienceId, data.Identity.Claims, issued.Value.UtcDateTime, expires.Value.UtcDateTime, signingKey);
// Create a handler
var handler = new JwtSecurityTokenHandler();
// Write our token string
var jwt = handler.WriteToken(token);
This returns what you would expect from the token (access_token, expires_in and token_type) but I would like to return some user information too. Things like username, roles, etc.
Anyone know how I can do this?
The username and roles are present in the authenticated identities claims and therefore persisted in the JWT that is sent back in the access token.
So the line:
var token = new JwtSecurityToken(_issuer, audienceId, data.Identity.Claims,issued,expires,signingKey);
Inserts the claims from the auth'd identity:
i.e if I did this in the OAuth provider:
```
IList<Claim> claims = new List<Claim>();
if (context.UserName.Equals("spencer") && context.UserName.Equals(context.Password))
{
claims.Add(new Claim(ClaimTypes.Name, user.DisplayName));
claims.Add(new Claim(ClaimTypes.Role, "User"));
}
));
var claimIdentity = new ClaimsIdentity(claims);
var ticket = new AuthenticationTicket(claimIdentity, null);
//Now authed and claims are in my identity context
context.Validated(ticket);
```
So now when the JWT is generated those claims are in the token.
You can then decorate your Api Controllers with explicit roles which would then query the "Roles" type in the claimset. If the user doesn't have the role in the role claimset than a 401 is issued:
```
[Route]
[Authorize(Roles ="User,Admin")]
public IHttpActionResult Get()
{
return Ok<IEnumerable<Product>>(_products);
}
[Route]
[Authorize(Roles = "Admin")]
public IHttpActionResult Post(Product product)
{
_products.Add(product);
return Created(string.Empty, product);
}
```
So in the above example, if I generate a JWT as me "Spencer" i'm in the users role and the GET would be OK (200) whilst the POST would be Unauthorized (401).
Make sense?

MVC - Mixed Auth - OWIN + Windows Auth

I need to have both windows authentication and owin (forms) authentication but i can't get it to work.
Probably the best option is to have two sites that have different authentication methods.
I found a project that does what i want: MVC5-MixedAuth. But it uses IISExpress and i can't get it to work with Local IIS.
The error that occurs is:
Request filtering is configured on the Web server to deny the request because the query string is too long.
If i remove all my ConfigureAuth() method inside Startup.Auth.cs it doesn't throw the error but i can't login because it is needed to do CookieAuthentication.
Startup.Auth.cs:
public void ConfigureAuth(IAppBuilder app)
{
app.CreatePerOwinContext(dbEmployeePortal.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, UserMaster, int>
(
validateInterval: TimeSpan.FromMinutes(30),
regenerateIdentityCallback: (manager, user) => user.GenerateUserIdentityAsync(manager),
getUserIdCallback: (id) => (Int32.Parse(id.GetUserId()))
)
}
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5));
app.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie);
}
Any idea?
UPDATE 1
The error
Request filtering is configured on the Web server to deny the request because the query string is too long.
appears because occurs a login loop when it tries to reach the login page.
Resolved!
I followed the example: MVC5-MixAuth
Credits: Mohammed Younes
UPDATE 1
Problem: I needed to have both Anonymous Authentication and Windows Authentication enabled.
But when you enable them both, you can only get NT AUTHORITY\IUSR.
Resolution: To get the current user (introduced with NTLM prompt), we need to create an handler that will execute when an user enter at login page.
When the user hits the login page, the handler will get the current windows identity cached in the browser and then set as the LogonUserIdentity.
Note: I needed to use windows first login, when the user hits the login page it will try to get the correspondent ASP.NET User.
Handler
using System.Threading.Tasks;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using Microsoft.AspNet.Identity;
namespace MixedAuth
{
/// <summary>
/// Managed handler for windows authentication.
/// </summary>
public class WindowsLoginHandler : HttpTaskAsyncHandler, System.Web.SessionState.IRequiresSessionState
{
public HttpContext Context { get; set; }
public override async Task ProcessRequestAsync(HttpContext context)
{
this.Context = context;
//if user is already authenticated, LogonUserIdentity will be holding the current application pool identity.
//to overcome this:
//1. save userId to session.
//2. log user off.
//3. request challenge.
//4. log user in.
if (context.User.Identity.IsAuthenticated)
{
this.SaveUserIdToSession(context.User.Identity.GetUserId());
await WinLogoffAsync(context);
context.RequestChallenge();
}
else if (!context.Request.LogonUserIdentity.IsAuthenticated)
{
context.RequestChallenge();
}
else
{
// true: user is trying to link windows login to an existing account
if (this.SessionHasUserId())
{
var userId = this.ReadUserIdFromSession();
this.SaveUserIdToContext(userId);
await WinLinkLoginAsync(context);
}
else // normal login.
await WinLoginAsync(context);
}
}
#region helpers
/// <summary>
/// Executes Windows login action against account controller.
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
private async Task WinLoginAsync(HttpContext context)
{
var routeData = this.CreateRouteData(Action.Login);
routeData.Values.Add("returnUrl", context.Request["returnUrl"]);
routeData.Values.Add("userName", context.Request.Form["UserName"]);
await ExecuteController(context, routeData);
}
/// <summary>
/// Execute Link Windows login action against account controller.
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
private async Task WinLinkLoginAsync(HttpContext context)
{
var routeData = this.CreateRouteData(Action.Link);
await ExecuteController(context, routeData);
}
/// <summary>
/// Executes Windows logoff action against controller.
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
private async Task WinLogoffAsync(HttpContext context)
{
var routeData = this.CreateRouteData(Action.Logoff);
await ExecuteController(context, routeData);
}
/// <summary>
/// Executes controller based on route data.
/// </summary>
/// <param name="context"></param>
/// <param name="routeData"></param>
/// <returns></returns>
private async Task ExecuteController(HttpContext context, RouteData routeData)
{
var wrapper = new HttpContextWrapper(context);
MvcHandler handler = new MvcHandler(new RequestContext(wrapper, routeData));
IHttpAsyncHandler asyncHandler = ((IHttpAsyncHandler)handler);
await Task.Factory.FromAsync(asyncHandler.BeginProcessRequest, asyncHandler.EndProcessRequest, context, null);
}
#endregion
}
}
Extensions
using System;
using System.Web;
using System.Web.Mvc;
using System.Web.Mvc.Html;
using System.Web.Routing;
namespace MixedAuth
{
public enum Action { Login, Link, Logoff };
public static class MixedAuthExtensions
{
const string userIdKey = "windows.userId";
//http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
const int fakeStatusCode = 418;
const string controllerName = "Account";
const string loginActionName = "WindowsLogin";
const string linkActionName = "LinkWindowsLogin";
const string logoffActionName = "WindowsLogoff";
const string windowsLoginRouteName = "Windows/Login";
public static void RegisterWindowsAuthentication(this MvcApplication app)
{
app.EndRequest += (object sender, EventArgs e) =>
{
HttpContext.Current.ApplyChallenge();
};
}
/// <summary>
/// Registers ignore route for the managed handler.
/// </summary>
/// <param name="routes"></param>
public static void IgnoreWindowsLoginRoute(this RouteCollection routes)
{
routes.IgnoreRoute(windowsLoginRouteName);
}
/// <summary>
/// By pass all middleware and modules, by setting a fake status code.
/// </summary>
/// <param name="context"></param>
public static void RequestChallenge(this HttpContext context)
{
context.Response.StatusCode = fakeStatusCode;
}
/// <summary>
/// Invoke on end response only. Replaces the current response status code with 401.2
/// </summary>
/// <param name="context"></param>
public static void ApplyChallenge(this HttpContext context)
{
if (context.Response.StatusCode == fakeStatusCode)
{
context.Response.StatusCode = 401;
context.Response.SubStatusCode = 2;
//http://msdn.microsoft.com/en-us/library/system.web.httpresponse.tryskipiiscustomerrors(v=vs.110).aspx
//context.Response.TrySkipIisCustomErrors = true;
}
}
/// <summary>
///
/// </summary>
/// <param name="handler"></param>
/// <param name="action"></param>
/// <returns></returns>
public static RouteData CreateRouteData(this WindowsLoginHandler handler, Action action)
{
RouteData routeData = new RouteData();
routeData.RouteHandler = new MvcRouteHandler();
switch (action)
{
case Action.Login:
routeData.Values.Add("controller", controllerName);
routeData.Values.Add("action", loginActionName);
break;
case Action.Link:
routeData.Values.Add("controller", controllerName);
routeData.Values.Add("action", linkActionName);
break;
case Action.Logoff:
routeData.Values.Add("controller", controllerName);
routeData.Values.Add("action", logoffActionName);
break;
default:
throw new NotSupportedException(string.Format("unknonw action value '{0}'.", action));
}
return routeData;
}
/// <summary>
/// Saves userId to the items collection inside <see cref="HttpContext"/>.
/// </summary>
public static void SaveUserIdToContext(this WindowsLoginHandler handler, string userId)
{
if (handler.Context.Items.Contains(userIdKey))
throw new ApplicationException("Id already exists in context.");
handler.Context.Items.Add("windows.userId", userId);
}
/// <summary>
/// Reads userId from item collection inside <see cref="HttpContext"/>.
/// </summary>
/// <remarks>The item will removed before this method returns</remarks>
/// <param name="context"></param>
/// <returns></returns>
public static int ReadUserId(this HttpContextBase context)
{
if (!context.Items.Contains(userIdKey))
throw new ApplicationException("Id not found in context.");
int userId = Convert.ToInt32(context.Items[userIdKey] as string);
context.Items.Remove(userIdKey);
return userId;
}
/// <summary>
/// Returns true if the session contains an entry for userId.
/// </summary>
public static bool SessionHasUserId(this WindowsLoginHandler handler)
{
return handler.Context.Session[userIdKey] != null;
}
/// <summary>
/// Save a session-state value with the specified userId.
/// </summary>
public static void SaveUserIdToSession(this WindowsLoginHandler handler, string userId)
{
if (handler.SessionHasUserId())
throw new ApplicationException("Id already exists in session.");
handler.Context.Session[userIdKey] = userId;
}
/// <summary>
/// Reads userId value from session-state.
/// </summary>
/// <remarks>The session-state value removed before this method returns.</remarks>
/// <param name="session"></param>
/// <returns></returns>
public static string ReadUserIdFromSession(this WindowsLoginHandler handler)
{
string userId = handler.Context.Session[userIdKey] as string;
if (string.IsNullOrEmpty(userIdKey))
throw new ApplicationException("Id not found in session.");
handler.Context.Session.Remove(userIdKey);
return userId;
}
/// <summary>
/// Creates a form for windows login, simulating external login providers.
/// </summary>
/// <param name="htmlHelper"></param>
/// <param name="htmlAttributes"></param>
/// <returns></returns>
public static MvcForm BeginWindowsAuthForm(this HtmlHelper htmlHelper, object htmlAttributes)
{
return htmlHelper.BeginForm("Login", "Windows", FormMethod.Post, htmlAttributes);
}
/// <summary>
/// Creates a form for windows login, simulating external login providers.
/// </summary>
/// <param name="htmlHelper"></param>
/// <param name="htmlAttributes"></param>
/// <returns></returns>
public static MvcForm BeginWindowsAuthForm(this HtmlHelper htmlHelper, object routeValues, object htmlAttributes)
{
return htmlHelper.BeginForm("Login", "Windows", FormMethod.Post, htmlAttributes);
}
}
}
Note
You need to have AccountController.cs as partial.
AccountController.Windows.cs
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using Microsoft.AspNet.Identity;
using MixedAuth;
namespace EmployeePortal.Web.Controllers
{
[Authorize]
public partial class AccountController : BaseController
{
//
// POST: /Account/WindowsLogin
[AllowAnonymous]
[AcceptVerbs(HttpVerbs.Get | HttpVerbs.Post)]
public ActionResult WindowsLogin(string userName, string returnUrl)
{
if (!Request.LogonUserIdentity.IsAuthenticated)
{
return RedirectToAction("Login");
}
var loginInfo = GetWindowsLoginInfo();
// Sign in the user with this external login provider if the user already has a login
var user = UserManager.Find(loginInfo);
if (user != null)
{
SignIn(user, isPersistent: false);
return RedirectToLocal(returnUrl);
}
else
{
return RedirectToAction("Login", new RouteValueDictionary(new { controller = "Account", action = "Login", returnUrl = returnUrl }));
}
}
//
// POST: /Account/WindowsLogOff
[HttpPost]
[ValidateAntiForgeryToken]
public void WindowsLogOff()
{
AuthenticationManager.SignOut();
}
//
// POST: /Account/LinkWindowsLogin
[AllowAnonymous]
[HttpPost]
public async Task<ActionResult> LinkWindowsLogin()
{
int userId = HttpContext.ReadUserId();
//didn't get here through handler
if (userId <= 0)
return RedirectToAction("Login");
HttpContext.Items.Remove("windows.userId");
//not authenticated.
var loginInfo = GetWindowsLoginInfo();
if (loginInfo == null)
return RedirectToAction("Manage");
//add linked login
var result = await UserManager.AddLoginAsync(userId, loginInfo);
//sign the user back in.
var user = await UserManager.FindByIdAsync(userId);
if (user != null)
await SignInAsync(user, false);
if (result.Succeeded)
return RedirectToAction("Manage");
return RedirectToAction("Manage", new { Message = ManageMessageId.Error });
}
#region helpers
private UserLoginInfo GetWindowsLoginInfo()
{
if (!Request.LogonUserIdentity.IsAuthenticated)
return null;
return new UserLoginInfo("Windows", Request.LogonUserIdentity.User.ToString());
}
#endregion
}
public class WindowsLoginConfirmationViewModel
{
[Required]
[Display(Name = "User name")]
public string UserName { get; set; }
}
}
Then, you need to add the handler:
<add name="Windows Login Handler" path="Login" verb="GET,POST" type="MixedAuth.WindowsLoginHandler" preCondition="integratedMode" />
Startup.cs
app.CreatePerOwinContext(dbEmployeePortal.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);
PathString path = new PathString("/Account/Login");
if (GlobalExtensions.WindowsAuthActive)
path = new PathString("/Windows/Login");
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
//LoginPath = new PathString("/Account/Login")
LoginPath = path
});
// Use a cookie to temporarily store information about a user logging in with a third party login provider
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
Then you need to configure Local IIS to use WindowsAuthentication and AnonymousAuthentication. You can do this in the Authentication Module.
Note If you don't have WindowsAuthentication go to Control Panel then Programs and Features then "Turn Windows features on or off":
select "Internet Information Services" > "World Wide Web" > "Security"
and select Windows Authentication.
I didn't see this in your answer, so for anyone looking on how to capture Windows Auth in your Owin pipeline, you also can add the following to your ConfigureAuth method:
public void ConfigureAuth(IAppBuilder app)
{
HttpListener listener = (HttpListener)app.Properties["System.Net.HttpListener"];
listener.AuthenticationSchemes = AuthenticationSchemes.IntegratedWindowsAuthentication;
}

Categories