Recently, I was struggling with the SSO authentication. At first I did pick up JSON Web Token which of course is a legitimate option, however, I was forced to share the secret key between different parties, as I decided to use HMAC. Not so long ago I decided to switch to the RSA instead and I’d like to present you both solutions using ASP.NET Core.
I will not dive into the details of how HMAC or RSA work as I’m not an expert in that matter, yet there’s at least this main difference that you should be aware of. If you stick to the HMAC, you’ll be forced to share the so-called secret key among different applications (e.g. services in a microservices architecture) given that you’d like to have the same valid token for different apps. And that’s not really the best idea, which is where RSA comes in handy – the service responsible for the token generation will use the private key for signing the JWT and all of the interested parties may use the public key to ensure the token’s validity. I did implement this solution for our open source platform Collectively that we’re building in a distributed way and we need sort of SSO mechanism in place.
Before we implement anything using C#, let’s prepare all of the stuff. In order to generate the secret key for HMAC, you could use e.g. this website. Just generate some random key, so it may look like this: GRQKzLUn9w59LpXEbsESa8gtJnN3hyspq7EV4J6Fz3FjBk994r.
Next, let’s create the RSA keys. In order to do that, we’ll use openssl tool. Open your terminal and type the following commands:
1 2 |
openssl genrsa -out private.pem 2048 openssl rsa -in private.pem -outform PEM -pubout -out public.pem |
It will result in creating 2 files, where the private key may look like this:
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 |
-----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEAzfU9wLR5CY3tSz258TcXmufSqHVzcB+q81FQZewAz7cB0ZHG O2+jrePkmb5sGhmFgWNGIh2Com1nzyVcTri1kKSXIWiF5R/29SieOacRCaOlNECF kavF44AYaUfthjtRDWNjKnCXDzvK0QLp16CyUZzzROy6QVIMXsoJpQbWUREz6HU7 jM3lZFxxI7Vvo68vPn0hx0H3JEUXBeUX9cVhBAX57lsNIhSFqypX49LuDM8SWrqS /sd3lJ8rg7YIPrbarym6ekrN7QG9UVhkJOoh1MqhI4qk30Nx1oqO64xQP6SnaEtk PAZRK6u4nX2r7TPqYb3LyXDWHFTC+6sd3dxaLwIDAQABAoIBAQCdY4XXV5MPTBhE YV1RCkrNo86F0Ytv6aNX4ZHQ8XMFSNLo9b9I+F1aq0asfqpZn5s4b0bPF0IXIggs cl6CAgEuEbk0XI3FtJGic3HGmPcaKKY8sfnggiXtXpxJCCBpbbbYxlSnv/aQO58X 7mQI1dKvL4Nv7n+/HxY48ahBJmJs+5tW4nh9OqCxnyDasBmj/ZBk52sVtQ+wkUVy itQZsyWx467U87i8v4wr+aPeDhP+WgAJlSDAPYi+q2/aQdMEctLOHMAhqzCgvSn6 kvM4DoIsC+atzu1fdR/saLBcXqmw3nMD1E6RVm7dHDoCbGkoE3ljXldaKMfCqlCy 7GPc07VBAoGBAOyVXizJGRF9Bj4g4mU8rq8WVEhzaiNGQi4cF5j5DGOHUq9ONrwY jmM1+tEu8xGnzIGxGwSbvcUf+MKakTMn7RpmY1h/QgaihQ84Q5e/99fri4e21akv 4kqBEV/4TFQWSGor1rfd4kiBtNfi6ooM5CqISIwJLD0q3z4AnV0g/fCxAoGBAN7c bhIhJBkKyBVqa/fjOoFZwODywzNAfKNGCQsIVw4Ap6IiGMavDADOKM9yR9S5ljza yoM0tGjrPRZesTi2iYMaiZq3qAtzGuGFrYt4vXGTDdRwksfHncKYtV0Xq4sYUUUw O3Or514+tPPk4FcgZdL5CfkTRQwz6OG1lM7ZjrDfAoGAK59PAgsCaEsZP5NoqyoJ O5duav188Iwf38imQTqKoj9ta42MYhpVBs4JNVDm2LaL6s3xIWRmFVbT024Un84Y 1elTIBo23mpRBoFlVTG8TT/NNnTr6Io/u2UZAw0RZd/F8m2q5bQv6Rahdb0Nae7+ kykV11xJn+2rxA7w9R8EM8ECgYEAjxBkXKEHwkeokA7kRpqJGTZb2kwdQQ55tHqm HX36HJQRCMTosMr4Yp/1lM4hDI8iwegWLsorslqouW6KSATuG8pyYW7aopb+v52H /cvBmWI0c5bcswES5jQP4TXrunwe19KRp7zH5zlMAnGADo5Or3ONkmZrYd0E97gQ UgVZU3MCgYBc1skIFPHHz82GUK9tK2HpiYocZDx/zPDVdn7lcGyVsLx+WSiEMDkC EkSg3dZA5QHXTLH4GKMdAmDvdle/FNXT7V1g62cObEEWi2TmJOc8TZdj07bTZmv0 qX8HfQ6ylUhSYXAI++kz3kRfYoRsUdeDF+Rpddzjk/Eh1wTSV8GVVQ== -----END RSA PRIVATE KEY----- |
And the public one like this:
1 2 3 4 5 6 7 8 9 |
-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzfU9wLR5CY3tSz258TcX mufSqHVzcB+q81FQZewAz7cB0ZHGO2+jrePkmb5sGhmFgWNGIh2Com1nzyVcTri1 kKSXIWiF5R/29SieOacRCaOlNECFkavF44AYaUfthjtRDWNjKnCXDzvK0QLp16Cy UZzzROy6QVIMXsoJpQbWUREz6HU7jM3lZFxxI7Vvo68vPn0hx0H3JEUXBeUX9cVh BAX57lsNIhSFqypX49LuDM8SWrqS/sd3lJ8rg7YIPrbarym6ekrN7QG9UVhkJOoh 1MqhI4qk30Nx1oqO64xQP6SnaEtkPAZRK6u4nX2r7TPqYb3LyXDWHFTC+6sd3dxa LwIDAQAB -----END PUBLIC KEY----- |
There’s one more thing before we can actually use our RSA keys within .NET Core application. We need to convert them into XML. It can be done here and before we copy our shiny XML into private-rsa-key.xml and public-rsa-key.xml files, let’s format them a little bit by using this tool. Eventually, we should have the following 2 files that we will deploy with our application:
1 2 3 4 5 6 7 8 9 10 |
<RSAKeyValue> <Modulus>zfU9wLR5CY3tSz258TcXmufSqHVzcB+q81FQZewAz7cB0ZHGO2+jrePkmb5sGhmFgWNGIh2Com1nzyVcTri1kKSXIWiF5R/29SieOacRCaOlNECFkavF44AYaUfthjtRDWNjKnCXDzvK0QLp16CyUZzzROy6QVIMXsoJpQbWUREz6HU7jM3lZFxxI7Vvo68vPn0hx0H3JEUXBeUX9cVhBAX57lsNIhSFqypX49LuDM8SWrqS/sd3lJ8rg7YIPrbarym6ekrN7QG9UVhkJOoh1MqhI4qk30Nx1oqO64xQP6SnaEtkPAZRK6u4nX2r7TPqYb3LyXDWHFTC+6sd3dxaLw==</Modulus> <Exponent>AQAB</Exponent> <P>7JVeLMkZEX0GPiDiZTyurxZUSHNqI0ZCLhwXmPkMY4dSr042vBiOYzX60S7zEafMgbEbBJu9xR/4wpqRMyftGmZjWH9CBqKFDzhDl7/31+uLh7bVqS/iSoERX/hMVBZIaivWt93iSIG01+LqigzkKohIjAksPSrfPgCdXSD98LE=</P> <Q>3txuEiEkGQrIFWpr9+M6gVnA4PLDM0B8o0YJCwhXDgCnoiIYxq8MAM4oz3JH1LmWPNrKgzS0aOs9Fl6xOLaJgxqJmreoC3Ma4YWti3i9cZMN1HCSx8edwpi1XRerixhRRTA7c6vnXj608+TgVyBl0vkJ+RNFDDPo4bWUztmOsN8=</Q> <DP>K59PAgsCaEsZP5NoqyoJO5duav188Iwf38imQTqKoj9ta42MYhpVBs4JNVDm2LaL6s3xIWRmFVbT024Un84Y1elTIBo23mpRBoFlVTG8TT/NNnTr6Io/u2UZAw0RZd/F8m2q5bQv6Rahdb0Nae7+kykV11xJn+2rxA7w9R8EM8E=</DP> <DQ>jxBkXKEHwkeokA7kRpqJGTZb2kwdQQ55tHqmHX36HJQRCMTosMr4Yp/1lM4hDI8iwegWLsorslqouW6KSATuG8pyYW7aopb+v52H/cvBmWI0c5bcswES5jQP4TXrunwe19KRp7zH5zlMAnGADo5Or3ONkmZrYd0E97gQUgVZU3M=</DQ> <InverseQ>XNbJCBTxx8/NhlCvbSth6YmKHGQ8f8zw1XZ+5XBslbC8flkohDA5AhJEoN3WQOUB10yx+BijHQJg73ZXvxTV0+1dYOtnDmxBFotk5iTnPE2XY9O202Zr9Kl/B30OspVIUmFwCPvpM95EX2KEbFHXgxfkaXXc45PxIdcE0lfBlVU=</InverseQ> <D>nWOF11eTD0wYRGFdUQpKzaPOhdGLb+mjV+GR0PFzBUjS6PW/SPhdWqtGrH6qWZ+bOG9GzxdCFyIILHJeggIBLhG5NFyNxbSRonNxxpj3GiimPLH54IIl7V6cSQggaW222MZUp7/2kDufF+5kCNXSry+Db+5/vx8WOPGoQSZibPubVuJ4fTqgsZ8g2rAZo/2QZOdrFbUPsJFFcorUGbMlseOu1PO4vL+MK/mj3g4T/loACZUgwD2Ivqtv2kHTBHLSzhzAIaswoL0p+pLzOA6CLAvmrc7tX3Uf7GiwXF6psN5zA9ROkVZu3Rw6AmxpKBN5Y15XWijHwqpQsuxj3NO1QQ==</D> </RSAKeyValue> |
1 2 3 4 |
<RSAKeyValue> <Modulus>zfU9wLR5CY3tSz258TcXmufSqHVzcB+q81FQZewAz7cB0ZHGO2+jrePkmb5sGhmFgWNGIh2Com1nzyVcTri1kKSXIWiF5R/29SieOacRCaOlNECFkavF44AYaUfthjtRDWNjKnCXDzvK0QLp16CyUZzzROy6QVIMXsoJpQbWUREz6HU7jM3lZFxxI7Vvo68vPn0hx0H3JEUXBeUX9cVhBAX57lsNIhSFqypX49LuDM8SWrqS/sd3lJ8rg7YIPrbarym6ekrN7QG9UVhkJOoh1MqhI4qk30Nx1oqO64xQP6SnaEtkPAZRK6u4nX2r7TPqYb3LyXDWHFTC+6sd3dxaLw==</Modulus> <Exponent>AQAB</Exponent> </RSAKeyValue> |
Just keep in mind, though, that the private key is required only for the service responsible for generating the token – the other parties should only use the public key.
Eventually, we can create a new Web API project and add the following packages into the .csproj file:
1 2 |
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="1.1.2" /> <PackageReference Include="System.Xml.XmlDocument" Version="4.3.0" /> |
Next, let’s create a new section within the apssettings.json and the C# class.
1 2 3 4 5 6 7 8 |
"jwt": { "issuer": "http://localhost:5000", "expiryDays": 3, "useRsa": true, "hmacSecretKey": "GRQKzLUn9w59LpXEbsESa8gtJnN3hyspq7EV4J6Fz3FjBk994r", "rsaPrivateKeyXml": "rsa-private-key.xml", "rsaPublicKeyXml": "rsa-public-key.xml" } |
1 2 3 4 5 6 7 8 9 |
public class JwtSettings { public string HmacSecretKey { get; set; } public int ExpiryDays { get; set; } public string Issuer { get; set; } public bool UseRsa { get; set; } public string RsaPrivateKeyXML { get; set; } public string RsaPublicKeyXML { get; set; } } |
We should also create the token model itself:
1 2 3 4 5 |
public class JWT { public string Token { get; set; } public long Expires { get; set; } } |
Now, let’s take care of the actual JWT handler, I’ll just paste the code here and it should be rather straightforward:
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 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
public interface IJwtHandler { JWT Create(string userId); TokenValidationParameters Parameters { get;} } public class JwtHandler : IJwtHandler { private readonly JwtSettings _settings; private readonly JwtSecurityTokenHandler _jwtSecurityTokenHandler = new JwtSecurityTokenHandler(); private SecurityKey _issuerSigningKey; private SigningCredentials _signingCredentials; private JwtHeader _jwtHeader; public TokenValidationParameters Parameters { get; private set; } public JwtHandler(IOptions<JwtSettings> settings) { _settings = settings.Value; if(_settings.UseRsa) { InitializeRsa(); } else { InitializeHmac(); } InitializeJwtParameters(); } private void InitializeRsa() { using(RSA publicRsa = RSA.Create()) { var publicKeyXml = File.ReadAllText(_settings.RsaPublicKeyXML); publicRsa.FromXmlString(publicKeyXml); _issuerSigningKey = new RsaSecurityKey(publicRsa); } if(string.IsNullOrWhiteSpace(_settings.RsaPrivateKeyXML)) { return; } using(RSA privateRsa = RSA.Create()) { var privateKeyXml = File.ReadAllText(_settings.RsaPrivateKeyXML); privateRsa.FromXmlString(privateKeyXml); var privateKey = new RsaSecurityKey(privateRsa); _signingCredentials = new SigningCredentials(privateKey, SecurityAlgorithms.RsaSha256); } } private void InitializeHmac() { _issuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_settings.HmacSecretKey)); _signingCredentials = new SigningCredentials(_issuerSigningKey, SecurityAlgorithms.HmacSha256); } private void InitializeJwtParameters() { _jwtHeader = new JwtHeader(_signingCredentials); Parameters = new TokenValidationParameters { ValidateAudience = false, ValidIssuer = _settings.Issuer, IssuerSigningKey = _issuerSigningKey }; } public JWT Create(string userId) { var nowUtc = DateTime.UtcNow; var expires = nowUtc.AddDays(_settings.ExpiryDays); var centuryBegin = new DateTime(1970, 1, 1); var exp = (long)(new TimeSpan(expires.Ticks - centuryBegin.Ticks).TotalSeconds); var now = (long)(new TimeSpan(nowUtc.Ticks - centuryBegin.Ticks).TotalSeconds); var issuer = _settings.Issuer ?? string.Empty; var payload = new JwtPayload { {"sub", userId}, {"unique_name", userId}, {"iss", issuer}, {"iat", now}, {"nbf", now}, {"exp", exp}, {"jti", Guid.NewGuid().ToString("N")} }; var jwt = new JwtSecurityToken(_jwtHeader, payload); var token = _jwtSecurityTokenHandler.WriteToken(jwt); return new JWT { Token = token, Expires = exp }; } } |
As you can see, such handler could be used by all of the services, as the private RSA key part is an optional one. Inside the payload you might notice a custom claim unique_name – this one is actually required if you want to get the current username using User.Identity.Name within ASP.NET Core application.
The FromXmlString() is an extension method defined in a following way:
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 |
public static void FromXmlString(this RSA rsa, string xmlString) { var parameters = new RSAParameters(); XmlDocument xmlDoc = new XmlDocument(); xmlDoc.LoadXml(xmlString); if (xmlDoc.DocumentElement.Name.Equals("RSAKeyValue")) { foreach (XmlNode node in xmlDoc.DocumentElement.ChildNodes) { switch (node.Name) { case "Modulus": parameters.Modulus = Convert.FromBase64String(node.InnerText); break; case "Exponent": parameters.Exponent = Convert.FromBase64String(node.InnerText); break; case "P": parameters.P = Convert.FromBase64String(node.InnerText); break; case "Q": parameters.Q = Convert.FromBase64String(node.InnerText); break; case "DP": parameters.DP = Convert.FromBase64String(node.InnerText); break; case "DQ": parameters.DQ = Convert.FromBase64String(node.InnerText); break; case "InverseQ": parameters.InverseQ = Convert.FromBase64String(node.InnerText); break; case "D": parameters.D = Convert.FromBase64String(node.InnerText); break; } } } else { throw new Exception("Invalid XML RSA key."); } rsa.ImportParameters(parameters); } |
Credits for this one go to that guy.
Let’s move forward as we’re about to finish the whole sample. We’ll start with creating a new controlle:
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 |
public class AccountController : Controller { private readonly IJwtHandler _jwtHandler; public AccountController(IJwtHandler jwtHandler) { _jwtHandler = jwtHandler; } [HttpGet("me")] [Authorize] public IActionResult Get() { return Content($"Hello {User.Identity.Name}"); } [HttpPost("sign-in")] public IActionResult SignIn([FromBody]SignIn request) { if(string.IsNullOrWhiteSpace(request.Username) || request.Password != "secret") { return Unauthorized(); } return Json(_jwtHandler.Create(request.Username)); } } |
Where the SignIn is a typical reqeust:
1 2 3 4 5 |
public class SignIn { public string Username { get; set; } public string Password { get; set; } } |
We’re almost done. Let’s open the Startup class and firstly extend the ConfigureServices():
1 2 |
services.Configure<JwtSettings>(Configuration.GetSection("jwt")); services.AddSingleton<IJwtHandler,JwtHandler>(); |
And move on to the Configure():
1 2 3 4 5 6 |
var jwtHandler = app.ApplicationServices.GetService<IJwtHandler>(); app.UseJwtBearerAuthentication(new JwtBearerOptions { AutomaticAuthenticate = true, TokenValidationParameters = jwtHandler.Parameters }); |
That’s it. Now you should be able to send the following requests in order to obtain the token and use it to access the secured endpoint. These are the samples using cURL:
1 2 3 |
curl localhost:5000/sign-in -X POST -H "content-type: application/json" -d '{"username": "spetz", "password": "secret"}' curl localhost:5000/me -H "Authorization: Bearer YOUR_JWT" |
You can switch between HMAC and RSA simply be setting useRsa boolean flag to either true or false. The actual server response for the successful sign in operation should be the following:
1 |
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzcGV0eiIsInVuaXF1ZV9uYW1lIjoic3BldHoiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJpYXQiOjE1MDA4MTY3NjksIm5iZiI6MTUwMDgxNjc2OSwiZXhwIjoxNTAxMjQ4NzY5LCJqdGkiOiI4NzA0MWY1NzliN2M0NzFhYTVmMjgzY2Y0NzA0MmM0OCJ9.6wDNloIkii0gry98aMge4c73brFwTCbOV-YXcDpf8k0","expires":1501248769} |
or
1 |
{"token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzcGV0eiIsInVuaXF1ZV9uYW1lIjoic3BldHoiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJpYXQiOjE1MDA4MTcwNDcsIm5iZiI6MTUwMDgxNzA0NywiZXhwIjoxNTAxMDc2MjQ3LCJqdGkiOiJkYjk1OWExYmUwMDc0ODk1YWMyMDdhZmRlYzM3OWM3YyJ9.KWAuNLlH2gwz1oujwSFKRAxbOpRhGy7jr5RvV4JH0DIhUBd2fTrHfC37_09B0dD3H3nqBWrrEdlyOODzeYyYcWym5pTsAPStgP9eIBrQH0fPubgrkkdLowAmnQWgwHy8Rqu7qkVx8kUP8zCFhmheVvlnlR87-pnZau1UJ_7bfsQuBNbiI6iAzp8TjjZXgvNYa_EwykQWr2kEgyYUkIUJhQgJvow7469l4vdTT27Ex3qSoN6IlBctS-LQPCIZvmxFT8kO5nWBtqeAqLsZmT63tdU2jfxKOweCn3n96UNJL78Y3cozRMtH_AQQb1MzAr_Dohur8PusHL7tyKQxCFZKXQ","expires":1501076247} |
The tokens can be valited here and the expiration date can be checked here, simply by coping the expires property being represented as the EPOCH ticks.
The full application and all of the source code can be downloaded from the GitHub repository. I hope that this one will get you started with JWT and SSO that can be used to achieve the robust token based authentication mechanism in your application.
Pingback: JWT RSA & HMAC + ASP.NET Core - How to Code .NET
Masz w planach rozszerzyć samą walidację JWT o refresh_token i skrócić czas tokena uwierzytelniającego ?
Gdy obecnie zmienimy jakieś dane w DB np zablokujemy usera to mimo wszystko będzie miał dostęp przez 3 dni zgodnie z powyższym przykładem ?
Myślałem o użyciu Identity Server 4 obecnie aczkolwiek jeszcze nie ma pełnego wsparcia na .NET Core 2.0 ;/ I mam tutaj zgrzyt o ile rozumiem cały schemat generowania tokena, wystawienie nowego tokena na podstawie refresh_token to mimo wszystko magia dla mnie, byłbyś w stanie jakoś to opisać jakie czynności należało by wykonać aby wystawić nowy token na podstawie refresh_tokena ? W jaką strukturę tutaj uderzyć ? Lub ewentualnie może masz w planach jakiś post na blogu ?
Identity Server 4 jest dla mnie zbyt duży i zrobienie czegoś niestandardowego wymaga masę pracy. Zastanawiałem się ostatnio nad mechanizem odświeżania tokena – na pewno jeszcze się rozpatrzę w temacie jak to inni implementują ale wstępny pomysł mam już gotowy. W bazie można trzymać prostą strukturę na zasadzie np. {token_hash, expires, refreshed}, czyli unikalny hash generowany na bazie tokena, data ekspiracji oraz flaga czy został odświeżony. Następnie pojedynczy endpoint typu POST gdzie użytkownik przekazuje token, następuje walidacja i dostaje w zwrotce nowy token. Jak to zaimplementuję to na pewno pomyślę nad postem na bloga :).
Czyli taki schemat:
1 Aplikacja loguje się przez API
2 API generuje token + refresh token
oraz zapisuje do db jako:
– token_hash (refresh_token),
– expires ( czas wygasniecia tokena )
– refreshed – czy token został już odswierzony
zwracamy token userowi
3. User wysyła żadanie o dostęp do danych
4. Następuje sprawdzenie czasu głównego tokena jeśli nie wygasł zwracamy dane
– jeśli wygasł sprawdzamy czy refresh token jeszcze nie wygasł w DB jeśli tak to zwracamy użytkownikowi brak autoryzacji
5. Jeśli token usera wygasł Aplikacja wysyła żądanie aby otrzymać nowy token
6. API sprawdza czy refresh token nie wygasł i zwraca nowy token ?
A co w przypadku jeśli ktoś fizycznie z przeglądarki wykradnie nasz token autoryzacyjny ? W takim przypadku dane użytkownika są narażone ?
Czy troszkę się pogubiłem ? Mógłbyś poprawny schemat taki rozpisać jak Ty to widzisz ?
Pozdrawiam 🙂
1. Logowanie przez API -> zwrócenie tokena i zapisanie jakiegoś unikalnego hasha w bazie (aby nie trzymać faktycznego tokena).
2. Prośba o odświeżenie – walidacja przez API czy token jest poprawny -> przekazanie do bazy, sprawdzenie czy refreshed == false i jeśli tak to zwrócenie nowego tokena (+ zapisanie w bazie jak w punkcie 1). W sumie w bazie tutaj już nie trzeba trzymać expires, bo to zweryfikuje API.
3. Jeśli token wygasł to trzeba zalogować się ponownie – wygasa po prostu na bazie “expires”, nie ma różnych dat do tego co jest w tokenie, a tego co w bazie.
Do tego stosuje się SSL, żeby nikt niczego nie wykradł.
Pingback: Dew Drop - July 24, 2017 (#2526) - Morning Dew
Great post! In my opinion RSA works best in scenarios where not all parties using the identity provider are going to be internal i.e third parties.
I think everyone having the same key is less of a concern if the correct dev ops/deployment practices are in place.
Thank you, Andrew. For sure, sharing a secret key is not a big deal if it’s used internally by the whole system, however, since e.g. only a single service should be responsible for the authentication, there’s no need to even give a chance to the other parties to be able to do the same just because they own the same secret key. And like you said RSA solves this issue and even more importantly you are able to share the public key with anyone else outside of your environment as well.
Hello Piotr,
Do you have any plans on doing tutorial how to do JWT authentication with refresh tokens on .NET Core 2.0 ?
Greetings John
Hi John,
I think it should be pretty much the same with .NET Core 2.0. Speaking of refreshing the tokens, it’s something that I’m going to implement and share on my blog in a near future.
W zasadzie w .net core 2.0 trochę zmieniła się struktura startup.cs jak i również zmieniła się trochę struktura własnych atrybutów autoryzujących.
Tutaj 1 z wątków odnośnie autoryzacji (breaking change)
https://github.com/aspnet/Announcements/issues/262
Tak samo zmieniła się kwestia jak wyżej napisałem własnych atrybutów autoryzujących. Nie mogłem niestety wątku na githubie znaleźć. Ale tam jest troszkę bardziej zawiła sprawa, gdyż trzeba teraz dodać własną politykę bezpieczeństwa.
Pozdrawiam Damian.
Dzięki, nie wiedziałem, że znowu wszystko pozmieniali, czasami brak słów na to co robią, kolejny powód, żeby się wstrzymać z aktualizacją :).
To może implementacja refresh token w .net core 2.0 żeby wszystko w miarę było na bieżąco ? I byłbyś w stanie też wytłumaczyć te własne polityki ? Z tego co znalazłem to można zrobić własny atrybut w ten sposób, troszkę dookoła ale zawsze coś 🙂 : https://www.codeproject.com/Articles/1171299/AttributeAuthorization-with-Custom-Roles-in-ASP-NE
Pozdrawiam Damian.
Policy omawiałem w jednym z odcinków kursu na YT, natomiast cokolwiek jest związane z .NET Core 2.0 na ten moment odpada, nie ufam niczemu co jest w wersji preview i pochodzi od MS. Odświeżanie tokenów jeśli już to zrobię dla 1.1, bo logika pod spodem będzie identyczna.
Mogę zapytać dlaczego ” natomiast cokolwiek jest związane z .NET Core 2.0 na ten moment odpada, nie ufam niczemu co jest w wersji preview i pochodzi od MS” ?
W kwartale 3 ma być pełna publikacja i w sumie niewiele czasu już zostało i zbliżamy się tak na prawdę do wersji finalnej. 2.0 Więc skąd taki dystans ? Kwestia bezpieczeństwa na produkcji, czy w samym momencie developmentu problemy i jeszcze niewielkie wsparcie community ?
Już niejednokrotnie się przejechałem na ich wersjach alpha/beta/preview i nie zamierzam powtarzać tego błędu ponownie, więc dopóki nie wyjdzie oficjalny release to nawet nie ruszam 2.0.
.NET Core 2.0 już wyszedł oficjalnie – https://blogs.msdn.microsoft.com/dotnet/2017/08/14/announcing-net-core-2-0/
Przymierzam się do przejścia na .NET Core, jednak na razie jestem na etapie prototypownia. Trochę informacji na temat refresh token’a znalazłem tu: http://bitoftech.net/2014/07/16/enable-oauth-refresh-tokens-angularjs-app-using-asp-net-web-api-2-owin/ jednak w oparciu o WebAPI 2. Implementacja w .NET Core 2.0 na pewno by pomogła 🙂
Tak, wiem ale jeszcze trochę czasu upłynie zanim w pełni zmigruję moje projekty na 2.0 żeby zacząć go w pełni używać, chociaż postaram się to zrobić możliwie szybko.
Awesome tutorial. I have a quick question. So when you set the expiry of JWT, how does it validate if the token is expired? I tested it, the old jwt tokens still work. Does it re validate a new token?
Thanks. Expired tokens should not be accepted at all – once the “exp” field is no longer valid, the API should return 401 by default. Make sure that you have enabled the expiry validation in the JWT settings.
Hi Piotr,
Firstly thanks for some awesome work, I have however been struggling with porting this to dotnetcore 2.0 (the porting process was okay, minor changes) – however most of the RSA requirements do not work on MacOS/Linux excepting for generating the JWT – during validation I get:
Microsoft.IdentityModel.Tokens.SecurityTokenInvalidSignatureException: IDX10503: Signature validation failed. Keys tried: ‘Microsoft.IdentityModel.Tokens.RsaSecurityKey , KeyId:
‘.
Exceptions caught:
”.
It appears as though the RSA key is not being read back from the JWT token (blank) ? Any ideas
Hi Grant,
Thanks, I’m happy to hear that.
Speaking of RSA – I also ran into issues with .NET Core 2.0 – you can take a look at the code here https://github.com/noordwind/Collectively.Common/blob/master/src/Collectively.Common/Security/JwtTokenHandler.cs
Within InitializeRsa() method I’m using the PemReader that comes from the ThirdParty.BouncyCastle.OpenSsl namespace. It seems to be working with typical certificates that are not transformed into XML files (as opposed to the public key above), yet I can’t guarantee that it will solve your issue.
Great Tutorial Piotr!
I wonder -since it has been a long time- are you planing to update RSA implementation with .NET Core 2.0 ?
I did everything as I can to convert it to NET Core 2.0, But I am getting silly 401 Unauhtorized errors. Maybe even some tips are appriciated.
thanks!
Thank you, Emre!
Actually, I stopped using RSA as I had some issues with it back then. I’m not sure whether there are any improvements, as there were quite a few hacks (as you can read in the post) needed to make it work.
I’ve just spend good portion of my day debugging this code. Summary is this, don’t do this
using(RSA publicRsa = RSA.Create())
{
var publicKeyXml = File.ReadAllText(_settings.RsaPublicKeyXML);
publicRsa.FromXmlString(publicKeyXml);
_issuerSigningKey = new RsaSecurityKey(publicRsa);
}
Instead do this
RSA publicRsa = RSA.Create()
var publicKeyXml = File.ReadAllText(_settings.RsaPublicKeyXML);
publicRsa.FromXmlString(publicKeyXml);
_issuerSigningKey = new RsaSecurityKey(publicRsa);
Otherwise you get dysfunctional _issuerSigningKey which has disposed RSA inside it. This leads to all sorts of undefined behaviour on Mac and maybe on Linux.
Thank you Andrey, I wasn’t aware of this, will keep that in mind!
Hi PIOTR,
Thanks for your helpful article.
I’m trying to implement your code in MVC 5 for an SSO project and so far everything has been working well and I’v reached to the point that authorization is successful.
However I’m a little confused. I’ll be grateful if you help me to solve my challenges.
1- First, I cannot understand where and how the public key is introduced to the system (OWIN), i.e. how does the system find and get the public key to do the authorization. Are the key-values of “rsaPrivateKeyXml” and “rsaPublicKeyXml” reserved for this or what?
2- My second question is somehow related to the first one. In my case, I need to separate the project that generates the token from other projects that consume the token and I don’t want the consumers to have the secret key (Private Key). Therefor, I need to know which portion of the code is necessary for consumers and which parts are necessary for the token producer.
3- My third question might seem silly because my cryptography information is not good. What confuses me is that, what I have read about RSA says that encryption in RSA is done with public key and the decryption is done with private key, but in your approach it is vice versa. Although your approach suits my case better since I don’t want other systems to be able to generate token, I would like to know why the definitions don’t match.
This article helped me a lot.
Could you update this portion of the code? In Core 2.1, an error occurred because the syntax is obsolete.
var jwtHandler = app.ApplicationServices.GetService();
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
AutomaticAuthenticate = true,
TokenValidationParameters = jwtHandler.Parameters
});
Thank you!