Welcome to the fifteenth episode of my course “Becoming a software developer” in which we will implement password encryption, authorization and authentication using JWT.
All of the materials including videos and sample projects can be downloaded from here.
The source code repository is being hosted on GitHub.
Scope
- Encryption
- Authentication
- Authorization
Abstract
Encryption
Whenever we want to store user accounts along with their passwords in our database, the best option is to apply a hashing function. It means that given e.g. “secret” password such function will create a so-called hash which can not be reversed (or decrypted) based on some random and secure sequence of characters named salt. Basically, whenever we want to ensure that the password is valid, we need to create its hash based on the salt generated for the first time when hashing the password e.g. during account registration and then compare the hashes, simple as that. This way, even if the data storage would be compromised the password can not be decrypted easily, as the hash is not a reversible function (at least theoretically).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
public interface IEncrypter { string GetSalt(string value); string GetHash(string value, string salt); } public class Encrypter : IEncrypter { private static readonly int DeriveBytesIterationsCount = 10000; private static readonly int SaltSize = 40; public string GetSalt(string value) { if (value.Empty()) { throw new ArgumentException("Can not generate salt from an empty value.", nameof(value)); } var random = new Random(); var saltBytes = new byte[SaltSize]; var rng = RandomNumberGenerator.Create(); rng.GetBytes(saltBytes); return Convert.ToBase64String(saltBytes); } public string GetHash(string value, string salt) { if (value.Empty()) { throw new ArgumentException("Can not generate hash from an empty value.", nameof(value)); } if (salt.Empty()) { throw new ArgumentException("Can not use an empty salt from hashing value.", nameof(value)); } var pbkdf2 = new Rfc2898DeriveBytes(value, GetBytes(salt), DeriveBytesIterationsCount); return Convert.ToBase64String(pbkdf2.GetBytes(SaltSize)); } private static byte[] GetBytes(string value) { var bytes = new byte[value.Length*sizeof(char)]; Buffer.BlockCopy(value.ToCharArray(), 0, bytes, 0, bytes.Length); return bytes; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public async Task LoginAsync(string email, string password) { var user = await _userRepository.GetAsync(email); if(user == null) { throw new Exception("Invalid credentials"); } var hash = _encrypter.GetHash(password, user.Salt); if(user.Password == hash) { return; } throw new Exception("Invalid credentials"); } |
Authentication
In order to find out the identity of the user in our system, he needs to be able to authenticate in some way. For the typical web application which is stateless, we can choose between different methods of authentication and pass along this information either with cookies, headers or within the URL itself. In our case, we want to use JWT (JSON Web Tokens) which is one of the most popular industry standards and basically boils down to generating a secure token that can be passed within the HTTP Header “Authorizaion: Bearer {token}”. Once the token is validated by the server, we can assign an identity to the user and allow him to perform operations that he wouldn’t be able to do otherwise.
1 2 3 4 5 6 7 8 9 10 |
app.UseJwtBearerAuthentication(new JwtBearerOptions { AutomaticAuthenticate = true, TokenValidationParameters = new TokenValidationParameters { ValidIssuer = "http://localhost:5000", ValidateAudience = false, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("super_secret_key123!")) } }); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
public class JwtHandler : IJwtHandler { private readonly JwtSettings _settings; public JwtHandler(JwtSettings settings) { _settings = settings; } public JwtDto CreateToken(Guid userId, string role) { var now = DateTime.UtcNow; var claims = new Claim[] { new Claim(JwtRegisteredClaimNames.Sub, userId.ToString()), new Claim(JwtRegisteredClaimNames.UniqueName, userId.ToString()), new Claim(ClaimTypes.Role, role), new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), new Claim(JwtRegisteredClaimNames.Iat, now.ToTimestamp().ToString(), ClaimValueTypes.Integer64) }; var expires = now.AddMinutes(_settings.ExpiryMinutes); var signingCredentials = new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_settings.Key)), SecurityAlgorithms.HmacSha256); var jwt = new JwtSecurityToken( issuer: _settings.Issuer, claims: claims, notBefore: now, expires: expires, signingCredentials: signingCredentials ); var token = new JwtSecurityTokenHandler().WriteToken(jwt); return new JwtDto { Token = token, Expires = expires.ToTimestamp() }; } } |
Authorization
Once the user was authenticated we can grant him access to the different operations or resources for example based on his role (user, moderator, admin etc.) or claims (list of permissions). While authentication is all about finding out if the user is who he claims to be, the authorization’s task is to validate whether the user has the required permissions to successfully perform a request.
1 |
services.AddAuthorization(x => x.AddPolicy("admin", p => p.RequireRole("admin"))); |
1 2 3 4 5 6 7 8 |
[Authorize(Policy = "admin")] [HttpPost] public async Task<IActionResult> Post([FromBody]CreateDriver command) { await DispatchAsync(command); return NoContent(); } |
Next
In the next expisode we will talk a little bit more about caching, implement the “login” endpoint in our API, move further with business logic and also resolve the user identity based on JWT claims and map it automatically to the commands that require user id.
Z tego co rozumiem salt to jest klucz który służy do zaszyfrowania hasła? Jeśli tak to nie rozumiem tego co zrobiłeś w klasach UserService oraz Encrypter. W UserService przy rejestracji użytkownika zapisujesz do bazy danych zaszyfrowane hasło + salt, gdzie salt, patrząc na wywołanie metody GetSalt sugerowałoby, że jest generowane na podstawie hasła, lecz patrząc w ciało tej metody widzę, że parametr value jest użyty jedynie do komunikatu, gdy jest on pusty, a salt jest jednak generowane losowo.
Tutaj pojawia się nieścisłość której nie mogę zrozumieć, z ciała metody GetSalt wynika, że salt jest generowane losowo, a nie na podstawie hasła jak wynika z wywołania tej metody (chyba, że coś przeoczyłem/czegoś nie rozumiem), a więc żeby przy logowaniu sprawdzić, czy hasło jest poprawne należało by odczytać salt z bazy i wygenerować na jego podstawie hash wpisanego hasła i go porównać z tym zapisanym w bazie. W klasie UserService jednak wygląda to inaczej, generujesz nowy salt, i na jego podstawie generujesz hash do porównania z hashem z bazy, czy to nie spowoduje sytuacji, że dwa takie same hasła będą miały inny hash?
Tak w skrócie:
Czy metoda GetSalt generuje salt losowo czy na podstawie hasła?
Dlaczego przy logowaniu generujesz nowy salt, zamiast pobrać go z bazy?
Ok, widzę, że w kolejnym odcinku jest wyjaśnienie 🙂
https://youtu.be/-SHQGWHl4g0?t=7m33s
Tak, to był błąd, który później sprostowałem :).
Witam
Ma pytanie. Po aktualizacji nie ma już dostępu do UseJwtBearerAuthentication(IApplicationBuilder, JwtBearerOptions) jakaś rada jak teraz wszystko połączyć?
dzieki
pozdrawiam
Powinno pomóc:
https://stackoverflow.com/questions/45686477/jwt-on-net-core-2-0
Tylko w moim przypadku w pliku Startup.cs w ConfigureServices(), w metodzie services.AddAuthentication() musiałem podać parametr JwtBearerDefaults.AuthenticationScheme:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
Mam takie pytanie, bo w .Net Core 2.0 app.UseJwtBearerAuthentication() w Configure() został zamieniony na services.AddAuthentication().AddJwtBearer() i przeniesiony do ConfigureServices() z której nie ma dostępu do pliku z ustawieniami w poniższy sposób app.ApplicationServices.GetService();
i dostęp do pliku rozwiązałem za pomocą:
Configuration.GetSection(“jwt:issuer”).Value
ale czy jest inna możliwość dostania się do tych ustawień? (dodanie IApplicationBuilder app do parametrów ConfigureServices() oczywiście wyrzuca błąd)
W komentarzach czasami usuwa się kod więc podrzucam link do kodu:
https://pastebin.com/F3UJc4pU
I jeszcze pytanie do powyższego kodu, jest jakaś różnica między liniami 46,47, a 53,54?
Jeszcze jedna rzecz wyszła w testach w .Net Core 2.0 nie czyta pliku appsettings.json, przez co nie może wczytać konfiguracji i się wysypuje przy services.AddAuthentication(), znalazłem taki wątek ale zbytnio mi nie pomógł:
https://github.com/aspnet/Hosting/issues/1191
Może miałeś podobny problem i masz na to jakieś rozwiązanie?
Nie wiem czy to dobre rozwiązanie ale jest napisana przez Piotra extension method do IConfiguration i można ja uzyć w ConfigureServices
var jwtSettings = Configuration.GetSettings();
Hey
Thanks for putting together this post on becoming a software developer – episode XV.It is a great read. I particularly find your thoughts about encryption interesting.
Keep up these insightful posts.
Cheers!