Проверка подлинности Asp.Net Core 1.0 с переходом на 2.0
У меня есть веб-API, который я хочу перенести в Asp.Net Core 2.0. API защищен, и я хочу перенести его на 2.0, потому что мы закончили первый круг. Я что-то пробовал, но когда я защищаю свой контроллер с помощью атрибута [Authenticate], контроллер в данной конечной точке никогда не вызывается, потому что пользователь не аутентифицирован.
public partial class Startup
{
public IConfigurationRoot Configuration { get; set; }
private static JwtOptions _jwtOptions;
private readonly IUserService _userService = new UserService();
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
Configuration = builder.Build();
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
//Add DI and other services
SetServices(services);
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedProto;
});
services.AddAuthentication(scheme =>
{
scheme.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = "aaa",
ValidAudience = "bbb",
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("secret key"))
};
});
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.ExpireTimeSpan = TimeSpan.FromDays(365);
options.Events = new CustomCookieAuthenticationEvents();
options.Cookie.Name = "access_token";
});
services.Configure<CookieAuthenticationOptions>(options =>
{
options.Events = new Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationEvents()
{
OnRedirectToLogin = ctx =>
{
if (ctx.Request.Path.StartsWithSegments("/api") && ctx.Response.StatusCode == 200)
{
ctx.Response.StatusCode = (int)System.Net.HttpStatusCode.Unauthorized;
return Task.FromResult<object>(null);
}
else
{
ctx.Response.Redirect(ctx.RedirectUri);
return Task.FromResult<object>(null);
}
}
};
});
//Logger
services.AddMvc(options =>
{
options.Filters.Add(new Loging.ApiExceptionFilter());
});
services.AddAuthentication(options =>
{
options.DefaultSignInScheme = JwtBearerDefaults.AuthenticationScheme;
});
// Add framework services.
MvcOptions mvcOptions = new MvcOptions();
mvcOptions.Filters.Add(new RequireHttpsAttribute());
MvcJsonOptions jsonOptions = new MvcJsonOptions();
jsonOptions.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
jsonOptions.SerializerSettings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore;
services.AddMvc(options => options = mvcOptions).AddJsonOptions(options => options = jsonOptions);
services.AddSession(options =>
{
// Set a short timeout for easy testing.
options.IdleTimeout = TimeSpan.FromDays(365);
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory, IOptions<JwtOptions> jwtOptions)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
app.UseDeveloperExceptionPage();
_jwtOptions = jwtOptions.Value;
app.UseAuthentication();
ConfigureAuth(app);
app.Map(new PathString("/api/images"), x => x.UseBlobFileViewHandler());
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseSession();
app.UseMvc(routes =>
{
routes.MapRoute
(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}"
);
routes.MapSpaFallbackRoute("spa-fallback", new { controller = "Home", action = "Index" });
});
}
Как вы можете видеть, я пытался внести некоторые изменения в класс startup.cs, но до сих пор не могу понять, как он работает. В документации везде есть EF. Как насчет нас, кто не хочет использовать реализацию EF.
public partial class Startup
{
private void ConfigureAuth(IApplicationBuilder app)
{
var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(_jwtOptions.SecretKey));
TokenProviderOptions tokenProviderOptions = new TokenProviderOptions
{
Path = "/api/token",
Audience = _jwtOptions.Audience,
Issuer = _jwtOptions.Issuer,
SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256),
IdentityResolver = GetIdentity
};
TokenValidationParameters tokenValidationParameters = new TokenValidationParameters
{
// The signing key must match!
ValidateIssuerSigningKey = true,
IssuerSigningKey = signingKey,
// Validate the JWT Issuer (iss) claim
ValidateIssuer = true,
ValidIssuer = _jwtOptions.Issuer,
// Validate the JWT Audience (aud) claim
ValidateAudience = true,
ValidAudience = _jwtOptions.Audience,
// Validate the token expiry
ValidateLifetime = true,
LifetimeValidator = LifetimeValidator,
// If you want to allow a certain amount of clock drift, set that here:
ClockSkew = TimeSpan.Zero
};
app.UseSimpleTokenProvider(tokenProviderOptions, tokenValidationParameters);
}
private Task<ClaimsIdentity> GetIdentity(string email)
{
ServiceMessage<UserEntity> request = _userService.FindByEmailAsync(email).Result;
if (request != null && request.Success && request.ResultObject != null)
{
return Task.FromResult(CreateClaimsIdentity(request.ResultObject, "Token"));
}
// Credentials are invalid, or account doesn't exist
return Task.FromResult<ClaimsIdentity>(null);
}
private ClaimsIdentity CreateClaimsIdentity(UserEntity user, string authenticationType)
{
List<Claim> claimCollection = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, user.Email, ClaimValueTypes.String),
new Claim(ClaimTypes.Role, user.Role, ClaimValueTypes.String),
new Claim(ClaimTypes.Name, user.Email.Split('@')[0], ClaimValueTypes.String),
new Claim(ClaimTypes.Expiration, DateTime.UtcNow.AddDays(365).Second.ToString(), ClaimValueTypes.DaytimeDuration)
};
ClaimsIdentity claimsIdentity = new ClaimsIdentity(claimCollection, authenticationType);
return claimsIdentity;
}
private bool LifetimeValidator(DateTime? notBefore, DateTime? expires, SecurityToken token, TokenValidationParameters @params)
{
if (expires != null)
{
return expires > DateTime.UtcNow;
}
return false;
}
public static string FromHex()
{
string hex = Guid.NewGuid().ToString();
hex = hex.Replace("-", "");
byte[] raw = new byte[hex.Length / 2];
for (int i = 0; i < raw.Length; i++)
{
raw[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16);
}
return Encoding.ASCII.GetString(raw);
}
}
public class CustomCookieAuthenticationEvents : CookieAuthenticationEvents
{
public override Task RedirectToLogin(RedirectContext<CookieAuthenticationOptions> context)
{
if (context.HttpContext.Request.Path.StartsWithSegments("/api") && context.HttpContext.Response.StatusCode == 200)
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
//return base.RedirectToLogin(context);
return Task.FromResult((int)HttpStatusCode.Unauthorized);
}
}
public class CustomJwtDataFormat : ISecureDataFormat<AuthenticationTicket>
{
private readonly string algorithm;
private readonly TokenValidationParameters validationParameters;
public CustomJwtDataFormat(string algorithm, TokenValidationParameters validationParameters)
{
this.algorithm = algorithm;
this.validationParameters = validationParameters;
}
public AuthenticationTicket Unprotect(string protectedText) => Unprotect(protectedText, null);
public AuthenticationTicket Unprotect(string protectedText, string purpose)
{
var handler = new JwtSecurityTokenHandler();
ClaimsPrincipal principal = null;
SecurityToken validToken = null;
try
{
principal = handler.ValidateToken(protectedText, this.validationParameters, out validToken);
var validJwt = validToken as JwtSecurityToken;
if (validJwt == null)
{
throw new ArgumentException("Invalid JWT");
}
if (!validJwt.Header.Alg.Equals(algorithm, StringComparison.Ordinal))
{
throw new ArgumentException($"Algorithm must be '{algorithm}'");
}
// Additional custom validation of JWT claims here (if any)
}
catch (SecurityTokenValidationException)
{
return null;
}
catch (ArgumentException)
{
return null;
}
// Validation passed. Return a valid AuthenticationTicket:
return new AuthenticationTicket(principal, new Microsoft.AspNetCore.Authentication.AuthenticationProperties(), "Cookie");
}
// This ISecureDataFormat implementation is decode-only
public string Protect(AuthenticationTicket data)
{
throw new NotImplementedException();
}
public string Protect(AuthenticationTicket data, string purpose)
{
throw new NotImplementedException();
}
}
}
public class TokenProviderMiddleware
{
private readonly RequestDelegate _next;
private readonly TokenProviderOptions _options;
private readonly ILogger _logger;
private readonly JsonSerializerSettings _serializerSettings;
private readonly TokenValidationParameters _tokenValidationParameters;
private readonly ISocialAuthentificationServices _socialAuthentificationServices;
public TokenProviderMiddleware(RequestDelegate next, IOptions<TokenProviderOptions> options, ILoggerFactory loggerFactory, ISocialAuthentificationServices socialAuthentificationServices, IOptions<TokenValidationParameters> tokenValidationParameters)
{
_socialAuthentificationServices = socialAuthentificationServices;
_next = next;
_logger = loggerFactory.CreateLogger<TokenProviderMiddleware>();
_options = options.Value;
_tokenValidationParameters = tokenValidationParameters.Value;
ThrowIfInvalidOptions(_options);
_serializerSettings = new JsonSerializerSettings
{
Formatting = Formatting.Indented
};
}
public Task Invoke(HttpContext context)
{
//Add CORS to every response
context.Response.Headers.Add("Access-Control-Allow-Headers", new string[] { "Authorization", "Content-Type" });
context.Response.Headers.Add("Access-Control-Allow-Methods", new string[] { "OPTIONS", "POST", "GET", "DELETE", "PUT" });
context.Response.Headers.Add("Access-Control-Allow-Origin", "*");
if (context.Request.Method.Equals("OPTIONS", StringComparison.Ordinal))
{
context.Response.StatusCode = 204;
return _next(context);
}
// If the request path doesn't match, skip
if (!context.Request.Path.Equals(_options.Path, StringComparison.Ordinal))
{
return _next(context);
}
// Request must be POST with Content-Type: application/x-www-form-urlencoded
if (!context.Request.Method.Equals("POST") || !context.Request.HasFormContentType)
{
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
return context.Response.WriteAsync("Bad request.");
}
_logger.LogInformation("Handling request: " + context.Request.Path);
return GetToken(context);
}
private async Task GetToken(HttpContext context)
{
TokenData headers = GetHeaderContext(context);
if (headers == null)
{
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
await context.Response.WriteAsync("Invalid encrypted token.");
return;
}
if (string.IsNullOrEmpty(headers.Provider))
{
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
await context.Response.WriteAsync("Provider not definied.");
return;
}
else if (string.IsNullOrEmpty(headers.Token))
{
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
await context.Response.WriteAsync("Invalid token.");
return;
}
else
{
var providers = (Mapurija.Models.Enum.Providers[])Enum.GetValues(typeof(Mapurija.Models.Enum.Providers));
if (!headers.Provider.Contains(headers.Provider))
{
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
await context.Response.WriteAsync("Invalid token.");
return;
}
}
ServiceMessage<UserEntity> validation = null;
int enumProvider = 0;
int.TryParse(headers.Provider, out enumProvider);
try
{
switch (enumProvider)
{
case (int)Mapurija.Models.Enum.Providers.Mapporia:
validation = await _socialAuthentificationServices.VerifyMapurijaTokenAsync(headers.Token);
break;
case (int)Mapurija.Models.Enum.Providers.Facebook:
validation = await _socialAuthentificationServices.VerifyFacebookTokenAsync(headers.Token);
break;
case (int)Mapurija.Models.Enum.Providers.Google:
validation = await _socialAuthentificationServices.VerifyFacebookTokenAsync(headers.Token);
break;
default:
validation = null;
break;
}
}
catch
{
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
await context.Response.WriteAsync("Invalid request token!");
return;
}
if (validation == null || !validation.Success || validation.ResultObject == null)
{
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
await context.Response.WriteAsync(validation.ErrorMessage);
return;
}
ClaimsIdentity identity = await _options.IdentityResolver(validation.ResultObject.Email);
if (identity == null)
{
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
await context.Response.WriteAsync("Invalid token.");
return;
}
DateTime now = DateTime.UtcNow;
// Specifically add the jti (nonce), iat (issued timestamp), and sub (subject/user) claims.
// You can add other claims here, if you want:
Claim[] claims = new Claim[]
{
new Claim(ClaimTypes.Name,validation.ResultObject.Email,ClaimValueTypes.String),
new Claim(JwtRegisteredClaimNames.Sub,validation.ResultObject.Email,ClaimValueTypes.String),
new Claim(JwtRegisteredClaimNames.Typ, validation.ResultObject.Role),
new Claim(JwtRegisteredClaimNames.Jti, await _options.NonceGenerator()),
new Claim(JwtRegisteredClaimNames.Iat, ToUnixEpochDate(now).ToString(), ClaimValueTypes.Integer64),
new Claim(JwtRegisteredClaimNames.Iss, _options.Issuer),
new Claim(JwtRegisteredClaimNames.Aud, _options.Audience)
};
// Create the JWT and write it to a string
JwtSecurityToken jwt = new JwtSecurityToken
(
issuer: _options.Issuer,
audience: _options.Audience,
claims: claims,
notBefore: now,
expires: now.Add(_options.Expiration),
signingCredentials: _options.SigningCredentials
);
SecurityToken token;
JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
string encodedJwt = handler.WriteToken(jwt);
ClaimsPrincipal principal = handler.ValidateToken(encodedJwt, _tokenValidationParameters, out token);
if (token == null)
{
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
await context.Response.WriteAsync("Invalid token generated!");
return;
}
var response = new
{
access_token = encodedJwt,
expires_in = (int)_options.Expiration.TotalSeconds
};
// Serialize and return the response
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(JsonConvert.SerializeObject(response, _serializerSettings));
}
private static void ThrowIfInvalidOptions(TokenProviderOptions options)
{
if (string.IsNullOrEmpty(options.Path))
{
throw new ArgumentNullException(nameof(TokenProviderOptions.Path));
}
if (string.IsNullOrEmpty(options.Issuer))
{
throw new ArgumentNullException(nameof(TokenProviderOptions.Issuer));
}
if (string.IsNullOrEmpty(options.Audience))
{
throw new ArgumentNullException(nameof(TokenProviderOptions.Audience));
}
if (options.Expiration == TimeSpan.Zero)
{
throw new ArgumentException("Must be a non-zero TimeSpan.", nameof(TokenProviderOptions.Expiration));
}
if (options.IdentityResolver == null)
{
throw new ArgumentNullException(nameof(TokenProviderOptions.IdentityResolver));
}
if (options.SigningCredentials == null)
{
throw new ArgumentNullException(nameof(TokenProviderOptions.SigningCredentials));
}
if (options.NonceGenerator == null)
{
throw new ArgumentNullException(nameof(TokenProviderOptions.NonceGenerator));
}
}
private TokenData GetHeaderContext(HttpContext context)
{
string token = new StreamReader(context.Request.Body).ReadToEnd();
if (string.IsNullOrEmpty(token))
{
return null;
}
var encrypted = Convert.FromBase64String(token);
var decriptedFromJavascript = Mapurija.Services.Common.TokenDecrypter.DecryptStringFromBytes(encrypted, Mapurija.Services.Common.TokenDecrypter.KeyBytes, Mapurija.Services.Common.TokenDecrypter.Vi);
TokenData result = JsonConvert.DeserializeObject< TokenData>(decriptedFromJavascript);
return result;
}
/// <summary>
/// Get this datetime as a Unix epoch timestamp (seconds since Jan 1, 1970, midnight UTC).
/// </summary>
/// <param name="date">The date to convert.</param>
/// <returns>Seconds since Unix epoch.</returns>
public static long ToUnixEpochDate(DateTime date) => (long)Math.Round((date.ToUniversalTime() - new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero)).TotalSeconds);
}
public class TokenProviderOptions
{
/// <summary>
/// The relative request path to listen on.
/// </summary>
/// <remarks>The default path is <c>/token</c>.</remarks>
public string Path { get; set; } = "api/token";
/// <summary>
/// The Issuer (iss) claim for generated tokens.
/// </summary>
public string Issuer { get; set; }
/// <summary>
/// The Audience (aud) claim for the generated tokens.
/// </summary>
public string Audience { get; set; }
/// <summary>
/// The expiration time for the generated tokens.
/// </summary>
/// <remarks>The default is five minutes (300 seconds).</remarks>
public TimeSpan Expiration { get; set; } = TimeSpan.FromDays(365);
/// <summary>
/// The signing key to use when generating tokens.
/// </summary>
public SigningCredentials SigningCredentials { get; set; }
/// <summary>
/// Resolves a user identity given a username and password.
/// </summary>
public Func<string, Task<ClaimsIdentity>> IdentityResolver { get; set; }
/// <summary>
/// Generates a random value (nonce) for each generated token.
/// </summary>
/// <remarks>The default nonce is a random GUID.</remarks>
public Func<Task<string>> NonceGenerator { get; set; } = new Func<Task<string>>(() => Task.FromResult(Guid.NewGuid().ToString()));