We are building Server for Web Api and Mobile Api using Bearer-Token with OWIN.
The requirements to validate are different between these two.
This is the Owin Configuration:
public void Configuration(IAppBuilder app)
{
app.MapSignalR();
ConfigureOAuth(app);
}
public void ConfigureOAuth(IAppBuilder app)
{
OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
{
TokenEndpointPath = new PathString("/Api/Token"),
Provider = new ApplicationOAuthProvider(),
// RefreshTokenProvider = new ApplicationRefreshTokenProvider(),
AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(8),
AllowInsecureHttp = true,
};
// Enable the application to use bearer tokens to authenticate users
app.UseOAuthBearerTokens(OAuthServerOptions);
OAuthAuthorizationServerOptions mobOAuthServerOptions = new OAuthAuthorizationServerOptions()
{
TokenEndpointPath = new PathString("/Mobile/Token"),
Provider = new MobileOAuthProvider(),
AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(20),
AllowInsecureHttp = true,
};
// Enable the application to use bearer tokens to authenticate users
app.UseOAuthBearerTokens(mobOAuthServerOptions);
}
This is the routing:
public class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "Api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
config.Routes.MapHttpRoute(
name: "MobileApi",
routeTemplate: "Mobile/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
// Ignore any authentication that happens before the request reaches the Web Api pipeline, either by IIS or OWIN middleware.
// That way to restrict Web Api to authenticate only using bearer tokens.
config.SuppressDefaultHostAuthentication();
// enable authentication using bearer tokens.
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
// configuration.Filters.Add(new TokenValidationAttribute());
}
}
Controller for API:
[Authorize]
public class EmployeeLeaveBalanceController : ApiBaseController
{
public EmployeeLeaveBalanceController()
: base()
{ }
[System.Web.Http.HttpGet]
public ApiResult GetLeaveBalance([FromUri] string payrollName, [FromUri] string employeeNumber)
{
try
{
SetContext(null);
ApiResult result = ValidateParameter(payrollName);
if (result != null)
return result;
EmployeeLeaveBalanceModel model = new EmployeeLeaveBalanceModel();
return model.GetLeaveBalance(payrollName, employeeNumber);
}
catch //(Exception)
{
return new ApiResult().SetMessage(ApiResultMessage.UnexceptedError);
}
}
private ApiResult ValidateParameter(string payrollName)
{
bool isValid = UploadHelper.GetPayroll(payrollName) != null;
return (!isValid)
? new ApiResult().SetMessage(ApiResultMessage.InvalidParameter)
: null;
}
}
For Mobile Api
[Authorize]
public class UserController : MobileBaseController
{
#region Get
[System.Web.Http.HttpGet]
public UserMdo Get()
{
try
{
SetContext();
UserModel model = new UserModel();
return model.GetUser(ApplicationContext.UserId);
}
catch(Exception)
{
throw;
// return new MobileResult().SetError(1000, ex.Message);
}
}
#endregion
}
However, I always get Unauthorized access when accessing Mobile Controller.
And Api Controller stopped working as well.
Did I miss anything? How do the controllers know that the bearer-token belongs to Web Api or Mobile Api? I think this is the problem.
This code to get the token and use it for authentication as well
Related
I am using ORY Kratos for identity and my frontend SPA (React App) is authenticating against the Kratos Login Server and gets a session cookie back.
Now I want to secure my ASP.NET Core Web Api in a way, that a user can only call certain methods protected with the [Authorize] attribute when attaching a valid cookie to the request. For this, I need to validate the cookie from every incoming request. So I am looking for a way to configure Authentication and add custom logic to validate the cookie (I need to make an API call to Kratos to validate it).
The cookie I want to validate has not been issued by the ASP.NET Core App that wants to validate it.
All the samples I found so far, are also issuing the cookie on the same server but I need to validate an external one.
This is what my cookie looks like:
In the Dev Tools, I can validate that the Cookie is attached to the requests header:
This is, what I've tried so far:
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.Cookie.Name = "ory_kratos_session";
options.Cookie.Path = "/";
options.Cookie.Domain = "localhost";
options.Cookie.HttpOnly = true;
options.EventsType = typeof(CustomCookieAuthenticationEvents);
});
services.AddScoped<CustomCookieAuthenticationEvents>();
// ...
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// ...
app.UseAuthentication();
app.UseAuthorization();
// ...
}
public class CustomCookieAuthenticationEvents : CookieAuthenticationEvents
{
public CustomCookieAuthenticationEvents() {}
public override async Task ValidatePrincipal(CookieValidatePrincipalContext context)
{
// Never gets called
}
}
Logs:
info: Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler[7]
Cookies was not authenticated. Failure message: Unprotect ticket failed
info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
Authorization failed. These requirements were not met:
DenyAnonymousAuthorizationRequirement: Requires an authenticated user.
info: Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler[12]
AuthenticationScheme: Cookies was challenged.
dbug: Microsoft.AspNetCore.Server.Kestrel[9]
Connection id "0HM6IBAO4PLLL" completed keep alive response.
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
Request finished HTTP/1.1 GET https://localhost:5001/weatherforecast - - - 302 0 - 75.3183ms
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
Request starting HTTP/1.1 GET https://localhost:5001/Account/Login?ReturnUrl=%2Fweatherforecast - -
According to the cookie authentication handler's source codes, I found it will read the cookie before goes to the CustomCookieAuthenticationEvents .
Some part of codes as below:
private async Task<AuthenticateResult> ReadCookieTicket()
{
var cookie = Options.CookieManager.GetRequestCookie(Context, Options.Cookie.Name!);
if (string.IsNullOrEmpty(cookie))
{
return AuthenticateResult.NoResult();
}
var ticket = Options.TicketDataFormat.Unprotect(cookie, GetTlsTokenBinding());
if (ticket == null)
{
return AuthenticateResult.Fail("Unprotect ticket failed");
}
if (Options.SessionStore != null)
{
var claim = ticket.Principal.Claims.FirstOrDefault(c => c.Type.Equals(SessionIdClaim));
if (claim == null)
{
return AuthenticateResult.Fail("SessionId missing");
}
// Only store _sessionKey if it matches an existing session. Otherwise we'll create a new one.
ticket = await Options.SessionStore.RetrieveAsync(claim.Value);
if (ticket == null)
{
return AuthenticateResult.Fail("Identity missing in session store");
}
_sessionKey = claim.Value;
}
var currentUtc = Clock.UtcNow;
var expiresUtc = ticket.Properties.ExpiresUtc;
if (expiresUtc != null && expiresUtc.Value < currentUtc)
{
if (Options.SessionStore != null)
{
await Options.SessionStore.RemoveAsync(_sessionKey!);
}
return AuthenticateResult.Fail("Ticket expired");
}
CheckForRefresh(ticket);
// Finally we have a valid ticket
return AuthenticateResult.Success(ticket);
}
If you still want to use cookie authentication, you need to rewrite the handler. So I suggest you could write a custom AuthenticationHandler and AuthenticationSchemeOptions class like below and register the class in the startup.cs directly. Then you could use [Authorize(AuthenticationSchemes = "Test")] to set the special AuthenticationSchemes.
Codes:
public class ValidateHashAuthenticationSchemeOptions : AuthenticationSchemeOptions
{
}
public class ValidateHashAuthenticationHandler
: AuthenticationHandler<ValidateHashAuthenticationSchemeOptions>
{
public ValidateHashAuthenticationHandler(
IOptionsMonitor<ValidateHashAuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
//TokenModel model;
// validation comes in here
if (!Request.Headers.ContainsKey("X-Base-Token"))
{
return Task.FromResult(AuthenticateResult.Fail("Header Not Found."));
}
var token = Request.Headers["X-Base-Token"].ToString();
try
{
// convert the input string into byte stream
using (MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(token)))
{
// deserialize stream into token model object
//model = Serializer.Deserialize<TokenModel>(stream);
}
}
catch (System.Exception ex)
{
Console.WriteLine("Exception Occured while Deserializing: " + ex);
return Task.FromResult(AuthenticateResult.Fail("TokenParseException"));
}
//if (model != null)
//{
// // success case AuthenticationTicket generation
// // happens from here
// // create claims array from the model
// var claims = new[] {
// new Claim(ClaimTypes.NameIdentifier, model.UserId.ToString()),
// new Claim(ClaimTypes.Email, model.EmailAddress),
// new Claim(ClaimTypes.Name, model.Name) };
// // generate claimsIdentity on the name of the class
// var claimsIdentity = new ClaimsIdentity(claims,
// nameof(ValidateHashAuthenticationHandler));
// // generate AuthenticationTicket from the Identity
// // and current authentication scheme
// var ticket = new AuthenticationTicket(
// new ClaimsPrincipal(claimsIdentity), this.Scheme.Name);
// // pass on the ticket to the middleware
// return Task.FromResult(AuthenticateResult.Success(ticket));
//}
return Task.FromResult(AuthenticateResult.Fail("Model is Empty"));
}
}
public class TokenModel
{
public int UserId { get; set; }
public string Name { get; set; }
public string EmailAddress { get; set; }
}
Startup.cs add below codes into the ConfigureServices method:
services.AddAuthentication(options =>
{
options.DefaultScheme
= "Test";
})
.AddScheme<ValidateHashAuthenticationSchemeOptions, ValidateHashAuthenticationHandler>
("Test", null);
Usage:
Controller:
[Authorize(AuthenticationSchemes = "Test")]
So I now solved it for myself differently by creating a custom Authentication Handler, which takes care of checking the Cookie that gets send to the Web API.
Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddSingleton(new KratosService("http://localhost:4433"));
services
.AddAuthentication("Kratos")
.AddScheme<KratosAuthenticationOptions, KratosAuthenticationHandler>("Kratos", null);
// ...
}
If you are interested in the implementation that did the job for me, I have attached the additional files below. It is also worth mentioning, that Kratos supports two way of Authenticating: Cookies and Bearer Tokens, depending on if you talk to it via Web App or an API. My implementation supports both. You can find a working Sample with ASP.NET Core and React here: https://github.com/robinmanuelthiel/kratos-demo
KratosAuthenticationHandler.cs:
using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace KratosDemo.Server.Kratos
{
public class KratosAuthenticationOptions : AuthenticationSchemeOptions
{
}
public class KratosAuthenticationHandler : AuthenticationHandler<KratosAuthenticationOptions>
{
readonly KratosService _kratosService;
readonly string _sessionCookieName = "ory_kratos_session";
public KratosAuthenticationHandler(
IOptionsMonitor<KratosAuthenticationOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock,
KratosService kratosService
)
: base(options, logger, encoder, clock)
{
_kratosService = kratosService;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
// ORY Kratos can authenticate against an API through two different methods:
// Cookie Authentication is for Browser Clients and sends a Session Cookie with each request.
// Bearer Token Authentication is for Native Apps and other APIs and sends an Authentication header with each request.
// We are validating both ways here by sending a /whoami request to ORY Kratos passing the provided authentication
// methods on to Kratos.
try
{
// Check, if Cookie was set
if (Request.Cookies.ContainsKey(_sessionCookieName))
{
var cookie = Request.Cookies[_sessionCookieName];
var id = await _kratosService.GetUserIdByCookie(_sessionCookieName, cookie);
return ValidateToken(id);
}
// Check, if Authorization header was set
if (Request.Headers.ContainsKey("Authorization"))
{
var token = Request.Headers["Authorization"];
var id = await _kratosService.GetUserIdByToken(token);
return ValidateToken(id);
}
// If neither Cookie nor Authorization header was set, the request can't be authenticated.
return AuthenticateResult.NoResult();
}
catch (Exception ex)
{
// If an error occurs while trying to validate the token, the Authentication request fails.
return AuthenticateResult.Fail(ex.Message);
}
}
private AuthenticateResult ValidateToken(string userId)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, userId),
};
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new System.Security.Principal.GenericPrincipal(identity, null);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return AuthenticateResult.Success(ticket);
}
}
}
KratosService.cs:
using System.Threading.Tasks;
using System.Net.Http;
using System.Text.Json;
using System;
namespace KratosDemo.Server.Kratos
{
public class KratosService
{
private readonly string _kratosUrl;
private readonly HttpClient _client;
public KratosService(string kratosUrl)
{
_client = new HttpClient();
_kratosUrl = kratosUrl;
}
public async Task<string> GetUserIdByToken(string token)
{
var request = new HttpRequestMessage(HttpMethod.Get, $"{_kratosUrl}/sessions/whoami");
request.Headers.Add("Authorization", token);
return await SendWhoamiRequestAsync(request);
}
public async Task<string> GetUserIdByCookie(string cookieName, string cookieContent)
{
var request = new HttpRequestMessage(HttpMethod.Get, $"{_kratosUrl}/sessions/whoami");
request.Headers.Add("Cookie", $"{cookieName}={cookieContent}");
return await SendWhoamiRequestAsync(request);
}
private async Task<string> SendWhoamiRequestAsync(HttpRequestMessage request)
{
var res = await _client.SendAsync(request);
res.EnsureSuccessStatusCode();
var json = await res.Content.ReadAsStringAsync();
var whoami = JsonSerializer.Deserialize<Whoami>(json);
if (!whoami.Active)
throw new InvalidOperationException("Session is not active.");
return whoami.Identity.Id;
}
}
}
Whoami.cs:
using System;
using System.Text.Json.Serialization;
namespace KratosDemo.Server.Kratos
{
public class Whoami
{
[JsonPropertyName("id")]
public string Id { get; set; }
[JsonPropertyName("active")]
public bool Active { get; set; }
[JsonPropertyName("expires_at")]
public DateTime ExpiresAt { get; set; }
[JsonPropertyName("authenticated_at")]
public DateTime AuthenticatedAt { get; set; }
[JsonPropertyName("issued_at")]
public DateTime IssuedAt { get; set; }
[JsonPropertyName("identity")]
public Identity Identity { get; set; }
}
public class Identity
{
[JsonPropertyName("id")]
public string Id { get; set; }
}
}
I thought I had a pretty simple goal in mind when I set out a day ago to implement a self-contained bearer auth webapi on .NET core 2.0, but I have yet to get anything remotely working. Here's a list of what I'm trying to do:
Implement a bearer token protected webapi
Issue tokens & refresh tokens from an endpoint in the same project
Use the [Authorize] attribute to control access to api surface
Not use ASP.Net Identity (I have much lighter weight user/membership reqs)
I'm totally fine with building identity/claims/principal in login and adding that to request context, but I've not seen a single example on how to issue and consume auth/refresh tokens in a Core 2.0 webapi without Identity. I've seen the 1.x MSDN example of cookies without Identity, but that didn't get me far enough in understanding to meet the requirements above.
I feel like this might be a common scenario and it shouldn't be this hard (maybe it's not, maybe just lack of documentation/examples?). As far as I can tell, IdentityServer4 is not compatible with Core 2.0 Auth, opendiddict seems to require Identity. I also don't want to host the token endpoint in a separate process, but within the same webapi instance.
Can anyone point me to a concrete example, or at least give some guidance as to what best steps/options are?
Did an edit to make it compatible with ASP.NET Core 2.0.
Firstly, some Nuget packages:
Microsoft.AspNetCore.Authentication.JwtBearer
Microsoft.AspNetCore.Identity
System.IdentityModel.Tokens.Jwt
System.Security.Cryptography.Csp
Then some basic data transfer objects.
// Presumably you will have an equivalent user account class with a user name.
public class User
{
public string UserName { get; set; }
}
public class JsonWebToken
{
public string access_token { get; set; }
public string token_type { get; set; } = "bearer";
public int expires_in { get; set; }
public string refresh_token { get; set; }
}
Getting into the proper functionality, you'll need a login/token web method to actually send the authorization token to the user.
[Route("api/token")]
public class TokenController : Controller
{
private ITokenProvider _tokenProvider;
public TokenController(ITokenProvider tokenProvider) // We'll create this later, don't worry.
{
_tokenProvider = tokenProvider;
}
public JsonWebToken Get([FromQuery] string grant_type, [FromQuery] string username, [FromQuery] string password, [FromQuery] string refresh_token)
{
// Authenticate depending on the grant type.
User user = grant_type == "refresh_token" ? GetUserByToken(refresh_token) : GetUserByCredentials(username, password);
if (user == null)
throw new UnauthorizedAccessException("No!");
int ageInMinutes = 20; // However long you want...
DateTime expiry = DateTime.UtcNow.AddMinutes(ageInMinutes);
var token = new JsonWebToken {
access_token = _tokenProvider.CreateToken(user, expiry),
expires_in = ageInMinutes * 60
};
if (grant_type != "refresh_token")
token.refresh_token = GenerateRefreshToken(user);
return token;
}
private User GetUserByToken(string refreshToken)
{
// TODO: Check token against your database.
if (refreshToken == "test")
return new User { UserName = "test" };
return null;
}
private User GetUserByCredentials(string username, string password)
{
// TODO: Check username/password against your database.
if (username == password)
return new User { UserName = username };
return null;
}
private string GenerateRefreshToken(User user)
{
// TODO: Create and persist a refresh token.
return "test";
}
}
You probably noticed the token creation is still just "magic" passed through by some imaginary ITokenProvider. Define the token provider interface.
public interface ITokenProvider
{
string CreateToken(User user, DateTime expiry);
// TokenValidationParameters is from Microsoft.IdentityModel.Tokens
TokenValidationParameters GetValidationParameters();
}
I implemented the token creation with an RSA security key on a JWT. So...
public class RsaJwtTokenProvider : ITokenProvider
{
private RsaSecurityKey _key;
private string _algorithm;
private string _issuer;
private string _audience;
public RsaJwtTokenProvider(string issuer, string audience, string keyName)
{
var parameters = new CspParameters { KeyContainerName = keyName };
var provider = new RSACryptoServiceProvider(2048, parameters);
_key = new RsaSecurityKey(provider);
_algorithm = SecurityAlgorithms.RsaSha256Signature;
_issuer = issuer;
_audience = audience;
}
public string CreateToken(User user, DateTime expiry)
{
JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();
ClaimsIdentity identity = new ClaimsIdentity(new GenericIdentity(user.UserName, "jwt"));
// TODO: Add whatever claims the user may have...
SecurityToken token = tokenHandler.CreateJwtSecurityToken(new SecurityTokenDescriptor
{
Audience = _audience,
Issuer = _issuer,
SigningCredentials = new SigningCredentials(_key, _algorithm),
Expires = expiry.ToUniversalTime(),
Subject = identity
});
return tokenHandler.WriteToken(token);
}
public TokenValidationParameters GetValidationParameters()
{
return new TokenValidationParameters
{
IssuerSigningKey = _key,
ValidAudience = _audience,
ValidIssuer = _issuer,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromSeconds(0) // Identity and resource servers are the same.
};
}
}
So you're now generating tokens. Time to actually validate them and wire it up. Go to your Startup.cs.
In ConfigureServices()
var tokenProvider = new RsaJwtTokenProvider("issuer", "audience", "mykeyname");
services.AddSingleton<ITokenProvider>(tokenProvider);
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => {
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = tokenProvider.GetValidationParameters();
});
// This is for the [Authorize] attributes.
services.AddAuthorization(auth => {
auth.DefaultPolicy = new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.Build();
});
Then Configure()
public void Configure(IApplicationBuilder app)
{
app.UseAuthentication();
// Whatever else you're putting in here...
app.UseMvc();
}
That should be about all you need. Hopefully I haven't missed anything.
The happy result is...
[Authorize] // Yay!
[Route("api/values")]
public class ValuesController : Controller
{
// ...
}
Following on #Mitch answer: Auth stack changed quite a bit moving to .NET Core 2.0. Answer below is just using the new implementation.
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
namespace JwtWithoutIdentity
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(cfg =>
{
cfg.RequireHttpsMetadata = false;
cfg.SaveToken = true;
cfg.TokenValidationParameters = new TokenValidationParameters()
{
ValidIssuer = "me",
ValidAudience = "you",
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("rlyaKithdrYVl6Z80ODU350md")) //Secret
};
});
services.AddMvc();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseAuthentication();
app.UseMvc();
}
}
}
Token Controller
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using JwtWithoutIdentity.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
namespace JwtWithoutIdentity.Controllers
{
public class TokenController : Controller
{
[AllowAnonymous]
[Route("api/token")]
[HttpPost]
public async Task<IActionResult> Token(LoginViewModel model)
{
if (!ModelState.IsValid) return BadRequest("Token failed to generate");
var user = (model.Password == "password" && model.Username == "username");
if (!user) return Unauthorized();
//Add Claims
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.UniqueName, "data"),
new Claim(JwtRegisteredClaimNames.Sub, "data"),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("rlyaKithdrYVl6Z80ODU350md")); //Secret
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken("me",
"you",
claims,
expires: DateTime.Now.AddMinutes(30),
signingCredentials: creds);
return Ok(new JsonWebToken()
{
access_token = new JwtSecurityTokenHandler().WriteToken(token),
expires_in = 600000,
token_type = "bearer"
});
}
}
}
Values Controller
using System.Collections.Generic;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace JwtWithoutIdentity.Controllers
{
[Route("api/[controller]")]
public class ValuesController : Controller
{
// GET api/values
[Authorize]
[HttpGet]
public IEnumerable<string> Get()
{
var name = User.Identity.Name;
var claims = User.Claims;
return new string[] { "value1", "value2" };
}
}
}
Hope this helps!
I added asp.net identity to my existing web project. now my controllers cannot be accessed. If I launch the local brower I am able to login in successfully. But when I go to one of the old controllers like
namespace PortwatchServiceV003.Controllers
{
[Authorize]
public class ValuesController : ApiController
{
// GET api/values
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
// GET api/values/5
public string Get(int id)
{
return "value";
}
// POST api/values
public void Post([FromBody]string value)
{
}
// PUT api/values/5
public void Put(int id, [FromBody]string value)
{
}
// DELETE api/values/5
public void Delete(int id)
{
}
}
}
it get error 404 not found.
Do the urls get somehow changed?
webapiconfig below...
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Services.Add(typeof(IExceptionLogger), new App_Start.AiExceptionLogger());
//config.EnableCors();
//var cors = new EnableCorsAttribute("http://localhost:57007", "*", "*");
//config.EnableCors(cors);
// Web API configuration and services
// Configure Web API to use only bearer token authentication.
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
//use https ONLY
//config.Filters.Add(new UseSSLAttribute());
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
var json = config.Formatters.JsonFormatter;
json.SerializerSettings.PreserveReferencesHandling = Newtonsoft.Json.PreserveReferencesHandling.Objects;
config.Formatters.Remove(config.Formatters.XmlFormatter);
config.Formatters.JsonFormatter.SerializerSettings.Formatting = Newtonsoft.Json.Formatting.Indented;
config.Formatters.JsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
}
}
I have a database, need web interface for administration and management (number of transactions, billing, and other administration) and to serve data from database (products) "manually" and API to serve data (products) to other bigger clients. All secured by SSL and https obviously.
I made a asp.net MVC 5 app (business logic and administration) and wanted to implement API (noob in API) for delivering data to users.
Have no idea how to implement security from MVC to API(same database).
The app is small and I can rewrite it. I'm thinking to try with core, but fear that i will be stuck with same problem.
Concrete question: What approach do I take and weather it should be in MVC 5 generation or .core (MVC 6) to be able to use one database for data, users and their authorizations?
(pushing everything true API is something I would like to avoid)
Ok, my project is done. I moved ahead on MVC 5.
(I apologize to you perfectionists, but I don't have the time now to strip unnecessary thing so I dumped whole files as they are :)
1st approach - abandoned
First I tried designing it as is recommended true the internet: .MVC solution, .DB for database and .API solution.
Conclusion - A lot of problems wit authentication and entity framework. At the end I abandoned this approach
2nd and successful approach
Just one solution .MVC
True NuGet installed .net Api, used integrated authorization extended using few tutorials (not single one worked). Note that I use ASP.NET Identity 2.0 Extending Identity Models and Using Integer Keys Instead of Strings and Implementing HTTPS Everywhere in ASP.Net MVC application.
Here are modifications and addons:
App_Start -> IdentityConfig.cs
public class ApplicationUserManager : UserManager<ApplicationUser, int>
{
// *** ADD INT TYPE ARGUMENT TO CONSTRUCTOR CALL:
public ApplicationUserManager(IUserStore<ApplicationUser, int> store)
: base(store)
{
}
public static ApplicationUserManager Create(
IdentityFactoryOptions<ApplicationUserManager> options,
IOwinContext context)
{
// *** PASS CUSTOM APPLICATION USER STORE AS CONSTRUCTOR ARGUMENT:
var manager = new ApplicationUserManager(
new ApplicationUserStore(context.Get<ApplicationDbContext>()));
// Configure validation logic for usernames
// *** ADD INT TYPE ARGUMENT TO METHOD CALL:
manager.UserValidator = new UserValidator<ApplicationUser, int>(manager)
{
AllowOnlyAlphanumericUserNames = false,
RequireUniqueEmail = true
};
// Configure validation logic for passwords
manager.PasswordValidator = new PasswordValidator
{
RequiredLength = 6,
RequireNonLetterOrDigit = false,
RequireDigit = true,
RequireLowercase = true,
RequireUppercase = true,
};
// other code removed for brevity
manager.UserLockoutEnabledByDefault = Convert.ToBoolean(ConfigurationManager.AppSettings["UserLockoutEnabledByDefault"].ToString());
manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(Double.Parse(ConfigurationManager.AppSettings["DefaultAccountLockoutTimeSpan"].ToString()));
manager.MaxFailedAccessAttemptsBeforeLockout = Convert.ToInt32(ConfigurationManager.AppSettings["MaxFailedAccessAttemptsBeforeLockout"].ToString());
// Register two factor authentication providers.
// This application uses Phone and Emails as a step of receiving a
// code for verifying the user You can write your own provider and plug in here.
// *** ADD INT TYPE ARGUMENT TO METHOD CALL:
//manager.RegisterTwoFactorProvider("PhoneCode",
// new PhoneNumberTokenProvider<ApplicationUser, int>
// {
// MessageFormat = "Your security code is: {0}"
// });
//// *** ADD INT TYPE ARGUMENT TO METHOD CALL:
//manager.RegisterTwoFactorProvider("EmailCode",
// new EmailTokenProvider<ApplicationUser, int>
// {
// Subject = "SecurityCode",
// BodyFormat = "Your security code is {0}"
// });
//manager.EmailService = new EmailService();
//manager.SmsService = new SmsService();
var dataProtectionProvider = options.DataProtectionProvider;
if (dataProtectionProvider != null)
{
// *** ADD INT TYPE ARGUMENT TO METHOD CALL:
manager.UserTokenProvider =
new DataProtectorTokenProvider<ApplicationUser, int>(
dataProtectionProvider.Create("ASP.NET Identity"));
}
return manager;
}
}
// PASS CUSTOM APPLICATION ROLE AND INT AS TYPE ARGUMENTS TO BASE:
public class ApplicationRoleManager : RoleManager<ApplicationRole, int>
{
// PASS CUSTOM APPLICATION ROLE AND INT AS TYPE ARGUMENTS TO CONSTRUCTOR:
public ApplicationRoleManager(IRoleStore<ApplicationRole, int> roleStore)
: base(roleStore)
{
}
// PASS CUSTOM APPLICATION ROLE AS TYPE ARGUMENT:
public static ApplicationRoleManager Create(
IdentityFactoryOptions<ApplicationRoleManager> options, IOwinContext context)
{
return new ApplicationRoleManager(
new ApplicationRoleStore(context.Get<ApplicationDbContext>()));
}
}
public class EmailService : IIdentityMessageService
{
public Task SendAsync(IdentityMessage message)
{
// Plug in your email service here to send an email.
return Task.FromResult(0);
}
}
public class SmsService : IIdentityMessageService
{
public Task SendAsync(IdentityMessage message)
{
// Plug in your sms service here to send a text message.
return Task.FromResult(0);
}
}
//This is useful if you do not want to tear down the database each time you run the application.
//public class ApplicationDbInitializer : DropCreateDatabaseAlways<ApplicationDbContext>
//This example shows you how to create a new database if the Model changes
public class ApplicationDbInitializer : DropCreateDatabaseIfModelChanges<ApplicationDbContext>
{
protected override void Seed(ApplicationDbContext context)
{
//InitializeIdentityForEF(context); //- Do not Seed - IGOR
//base.Seed(context);
}
//Create User=Admin#Admin.com with password=Admin#123456 in the Admin role
//public static void InitializeIdentityForEF(ApplicationDbContext db)
//{
// var userManager = HttpContext.Current.GetOwinContext().GetUserManager<ApplicationUserManager>();
// var roleManager = HttpContext.Current.GetOwinContext().Get<ApplicationRoleManager>();
// const string name = "igor#email.mail";
// const string password = "LolLol1";
// const string roleName = "lol";
// //Create Role Admin if it does not exist
// var role = roleManager.FindByName(roleName);
// if (role == null)
// {
// role = new ApplicationRole(roleName);
// var roleresult = roleManager.Create(role);
// }
// var user = userManager.FindByName(name);
// if (user == null)
// {
// user = new ApplicationUser { UserName = name, Email = name };
// var result = userManager.Create(user, password);
// result = userManager.SetLockoutEnabled(user.Id, false);
// }
// // Add user admin to Role Admin if not already added
// var rolesForUser = userManager.GetRoles(user.Id);
// if (!rolesForUser.Contains(role.Name))
// {
// var result = userManager.AddToRole(user.Id, role.Name);
// }
//}
}
public class ApplicationSignInManager : SignInManager<ApplicationUser, int>
{
public ApplicationSignInManager(ApplicationUserManager userManager, IAuthenticationManager authenticationManager) :
base(userManager, authenticationManager)
{ }
public override Task<ClaimsIdentity> CreateUserIdentityAsync(ApplicationUser user)
{
return user.GenerateUserIdentityAsync((ApplicationUserManager)UserManager);
}
public static ApplicationSignInManager Create(IdentityFactoryOptions<ApplicationSignInManager> options, IOwinContext context)
{
return new ApplicationSignInManager(context.GetUserManager<ApplicationUserManager>(), context.Authentication);
}
}
App_Start -> Startup.Auth.cs
public partial class Startup
{
public static OAuthAuthorizationServerOptions OAuthOptions { get; private set; }
public static string PublicClientId { get; private set; }
public void ConfigureAuth(IAppBuilder app)
{
// Configure the db context, user manager and role manager to use a single instance per request
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.CreatePerOwinContext<ApplicationRoleManager>(ApplicationRoleManager.Create);
app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);
// 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
// Configure the sign in cookie
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
// Enables the application to validate the security stamp when the user logs in.
// This is a security feature which is used when you change a password or add an external login to your account.
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser, int>(
validateInterval: TimeSpan.FromMinutes(2880),
regenerateIdentityCallback: (manager, user) => user.GenerateUserIdentityAsync(manager),
// Need to add THIS line because we added the third type argument (int) above:
getUserIdCallback: (claim) => int.Parse(claim.GetUserId()))
}
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
// Enables the application to temporarily store user information when they are verifying the second factor in the two-factor authentication process.
app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5));
// Enables the application to remember the second login verification factor such as phone or email.
// Once you check this option, your second step of verification during the login process will be remembered on the device where you logged in from.
// This is similar to the RememberMe option when you log in.
app.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie);
// Uncomment the following lines to enable logging in with third party login providers
//app.UseMicrosoftAccountAuthentication(
// clientId: "",
// clientSecret: "");
//app.UseTwitterAuthentication(
// consumerKey: "",
// consumerSecret: "");
//app.UseFacebookAuthentication(
// appId: "",
// appSecret: "");
//app.UseGoogleAuthentication(
// clientId: "",
// clientSecret: "");
// Configure the application for OAuth based flow
PublicClientId = "self";
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/Token"),
Provider = new ApplicationOAuthProvider(PublicClientId),
AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"), //TODO - makni ovo
AccessTokenExpireTimeSpan = TimeSpan.FromHours(1),
// In production mode set AllowInsecureHttp = false
AllowInsecureHttp = true
};
// Enable the application to use bearer tokens to authenticate users
app.UseOAuthBearerTokens(OAuthOptions);
}
}
//public partial class Startup
//{
// public void ConfigureAuth(IAppBuilder app)
// {
// // Enable the application to use a cookie to store information for the signed in user
// app.UseCookieAuthentication(new CookieAuthenticationOptions
// {
// ExpireTimeSpan = TimeSpan.FromHours(24),
// CookieSecure = CookieSecureOption.Never,
// CookieHttpOnly = false,
// SlidingExpiration = true,
// AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
// LoginPath = new PathString("/Account/Login")
// });
// // Use a cookie to temporarily store information about a user logging in with a third party login provider
// app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
// }
//}
App_Start -> WebApiConfig.cs
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// TODO: Add any additional configuration code.
// Web API routes
config.MapHttpAttributeRoutes();
//config.Routes.MapHttpRoute(
// name: "getkey",
// routeTemplate: "api/ApiKeys/Get/{term}"
//);
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
// WebAPI when dealing with JSON & JavaScript!
// Setup json serialization to serialize classes to camel (std. Json format)
var formatter = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
formatter.SerializerSettings.ContractResolver =
new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver();
// make all web-api requests to be sent over https
config.MessageHandlers.Add(new EnforceHttpsHandler());
}
}
MySysAdmin controller that I use for initial insert and edit of roles and initial user.
public SysAdminController(ApplicationUserManager userManager,
ApplicationRoleManager roleManager)
{
UserManager = userManager;
RoleManager = roleManager;
}
private ApplicationUserManager _userManager;
public ApplicationUserManager UserManager
{
get
{
return _userManager ?? HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();
}
set
{
_userManager = value;
}
}
private ApplicationRoleManager _roleManager;
public ApplicationRoleManager RoleManager
{
get
{
return _roleManager ?? HttpContext.GetOwinContext().Get<ApplicationRoleManager>();
}
private set
{
_roleManager = value;
}
}
public ActionResult RoleIndex()
{
return View(RoleManager.Roles);
}
public ActionResult RoleCreate()
{
return View();
}
[HttpPost]
public async Task<ActionResult> RoleCreate(SysAdminVM.RoleViewModel roleViewModel)
{
if (ModelState.IsValid)
{
// Use ApplicationRole, not IdentityRole:
var role = new ApplicationRole(roleViewModel.Name);
var roleresult = await RoleManager.CreateAsync(role);
if (!roleresult.Succeeded)
{
ModelState.AddModelError("", roleresult.Errors.First());
return View();
}
return RedirectToAction("RoleIndex");
}
return View();
}
public async Task<ActionResult> RoleEdit(int id)
{
if (id > 0)
{
var role = await RoleManager.FindByIdAsync(id);
if (role == null)
{
return HttpNotFound();
}
SysAdminVM.RoleViewModel roleModel = new SysAdminVM.RoleViewModel { Id = role.Id, Name = role.Name };
return View(roleModel);
}
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> RoleEdit([Bind(Include = "Name,Id")] SysAdminVM.RoleViewModel roleModel)
{
if (ModelState.IsValid)
{
var role = await RoleManager.FindByIdAsync(roleModel.Id);
role.Name = roleModel.Name;
await RoleManager.UpdateAsync(role);
return RedirectToAction("RoleIndex");
}
return View();
}
[AllowAnonymous]
public async Task<ActionResult> Initialize()
{
if (db.App.Where(x => x.Name.Contains("Initialize")).FirstOrDefault() == null)
{
await InitRoleCreate();
await InitUser();
db.App.Add(
new App { Name = "Initialize", Val = "true" }
);
db.SaveChanges();
return View();
}
return HttpNotFound();
}
private async Task InitRoleCreate()
{
var model = new List<string>()
{
"SysAdmin",
"Admin",
"User",
};
foreach (var item in model)
{
var role = new ApplicationRole(item);
await RoleManager.CreateAsync(role);
}
}
private async Task InitUser()
{
var user = new ApplicationUser
{
UserName = "HerGiz",
Email = "hergiz#outlook.com",
Name = "Igor Hermanović",
Contact = "098 185 3131",
TwoFactorEnabled = false,
LockoutEnabled = true,
EmailConfirmed = true
};
var adminResult = await UserManager.CreateAsync(user, "W7xtc2ywfb");
await UserManager.AddToRolesAsync(user.Id, "SysAdmin");
}
}
Entire API part that I need - controller and out of the box login (that is berried somewhere):
[Authorize]
public class ApiKeysController : ApiController
{
[Authorize]
[Route("api/getkey/{term}")]
public ShowFullKeyVM Get(string term)
{
if (User.Identity.IsAuthenticated == true)
{
if (!string.IsNullOrWhiteSpace(term) && (term.Length == 15 || term.Length == 16))
{
var lKey = new LKey();
var vm = lKey.Search(term);
if (vm != null)
{
return vm;
}
}
return new ShowFullKeyVM() { Error = "IMEI either is not valid :(", SearchIMEI = term };
}
return new ShowFullKeyVM() { Error = "Not Authenticated!!!", SearchIMEI = term };
}
}
Global.asax
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
GlobalConfiguration.Configure(WebApiConfig.Register);
MvcHandler.DisableMvcResponseHeader = true;
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
App_Start.AutoMapperConfig.DefineMaps();
ModelBinders.Binders.Add(typeof(decimal), new Extensions.DecimalModelBinder());
ModelBinders.Binders.Add(typeof(decimal?), new Extensions.DecimalModelBinder());
}
Web.config
<appSettings>
<add key="UserLockoutEnabledByDefault" value="true" />
<add key="DefaultAccountLockoutTimeSpan" value="30" />
<add key="MaxFailedAccessAttemptsBeforeLockout" value="4" />
</appSettings>
I am updating from ASP.NET MVC Identity 1 to 2 and I am using MVC 5. Everything worked fine when sending the email to finalise registering in Identity 1 but since updating I am getting this error message:
No IUserTokenProvider is registered.
From researching I think that I need to put this code into my application but I do not know where to put it?
var provider = new Microsoft.Owin.Security.DataProtection.DpapiDataProtectionProvider("PaymentPortal");
UserManager<ApplicationUser> userManager = new UserManager<ApplicationUser>(new UserStore<ApplicationUser>());
userManager.UserTokenProvider = new Microsoft.AspNet.Identity.Owin.DataProtectorTokenProvider<ApplicationUser>(provider.Create("EmailConfirmation"));
I suggest you to create CustomUserManager like below and register it owin startup, because then you have power to configure adentity configuration in one place.
public class CustomUserManager : UserManager<ApplicationUser>
{
public CustomUserManager(IUserStore<ApplicationUser> store) : base(store)
{
}
public static CustomUserManager Create(IdentityFactoryOptions<CustomUserManager> options, IOwinContext context)
{
var manager = new CustomUserManager(new UserStore<ApplicationUser>(context.Get<YourDataContenxt>()));
// Configure validation logic for usernames
manager.UserValidator = new UserValidator<ApplicationUser>(manager)
{
AllowOnlyAlphanumericUserNames = false,
RequireUniqueEmail = true
};
// Configure validation logic for passwords
manager.PasswordValidator = new PasswordValidator
{
RequiredLength = 6,
RequireNonLetterOrDigit = false,
RequireDigit = true,
RequireLowercase = true,
RequireUppercase = true,
};
// Configure user lockout defaults
manager.UserLockoutEnabledByDefault = false;
manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5);
//manager.MaxFailedAccessAttemptsBeforeLockout = 5;
// Register two factor authentication providers. This application uses Phone and Emails as a step of receiving a code for verifying the user
// You can write your own provider and plug in here.
manager.RegisterTwoFactorProvider("PhoneCode", new PhoneNumberTokenProvider<ApplicationUser, string>
{
MessageFormat = "Your security code is: {0}"
});
// Two Factor Authentication
manager.RegisterTwoFactorProvider("EmailCode", new EmailTokenProvider<ApplicationUser, string>
{
Subject = "SecurityCode",
BodyFormat = "Your security code is {0}"
});
// Your Email service
//manager.EmailService = new IdentityEmailMessageService(new EmailService(new MailServer()));
//manager.SmsService = new SmsService();
// Data Protection Provider
var dataProtectionProvider = options.DataProtectionProvider;
if (dataProtectionProvider != null)
{
manager.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser>(dataProtectionProvider.Create("Asp.NEt Identity"));
}
return manager;
}
}
}
Then configure the db context and user manager to use a single instance per request Startup.Auth file.
public partial class Startup
{
public static OAuthAuthorizationServerOptions OAuthOptions { get; private 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)
{
// Configure the db context and user manager to use a single instance per request
app.CreatePerOwinContext(YourDataContext.Create);
app.CreatePerOwinContext<CustomUserManager>(CustomUserManager.Create);
// Other relevant configurations
}
}
In the controller you can get the instance as below.
private CustomUserManager customUserManager;
public CustomUserManager CustomUserManager
{
get
{
return customUserManager?? HttpContext.GetOwinContext().GetUserManager<CustomUserManager >();
}
private set
{
customUserManager= value;
}
}
Hope this helps.