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:

openssl genrsa -out private.pem 2048 openssl rsa -in private.pem -outform PEM -pubout -out public.pem 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:

-----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----- 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:

-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzfU9wLR5CY3tSz258TcX mufSqHVzcB+q81FQZewAz7cB0ZHGO2+jrePkmb5sGhmFgWNGIh2Com1nzyVcTri1 kKSXIWiF5R/29SieOacRCaOlNECFkavF44AYaUfthjtRDWNjKnCXDzvK0QLp16Cy UZzzROy6QVIMXsoJpQbWUREz6HU7jM3lZFxxI7Vvo68vPn0hx0H3JEUXBeUX9cVh BAX57lsNIhSFqypX49LuDM8SWrqS/sd3lJ8rg7YIPrbarym6ekrN7QG9UVhkJOoh 1MqhI4qk30Nx1oqO64xQP6SnaEtkPAZRK6u4nX2r7TPqYb3LyXDWHFTC+6sd3dxa LwIDAQAB -----END PUBLIC KEY----- 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:

<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 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 >

<RSAKeyValue> <Modulus>zfU9wLR5CY3tSz258TcXmufSqHVzcB+q81FQZewAz7cB0ZHGO2+jrePkmb5sGhmFgWNGIh2Com1nzyVcTri1kKSXIWiF5R/29SieOacRCaOlNECFkavF44AYaUfthjtRDWNjKnCXDzvK0QLp16CyUZzzROy6QVIMXsoJpQbWUREz6HU7jM3lZFxxI7Vvo68vPn0hx0H3JEUXBeUX9cVhBAX57lsNIhSFqypX49LuDM8SWrqS/sd3lJ8rg7YIPrbarym6ekrN7QG9UVhkJOoh1MqhI4qk30Nx1oqO64xQP6SnaEtkPAZRK6u4nX2r7TPqYb3LyXDWHFTC+6sd3dxaLw==</Modulus> <Exponent>AQAB</Exponent> </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:

<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="1.1.2" /> <PackageReference Include="System.Xml.XmlDocument" Version="4.3.0" /> 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.

"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 "jwt" : { "issuer" : "http://localhost:5000" , "expiryDays" : 3 , "useRsa" : true , "hmacSecretKey" : "GRQKzLUn9w59LpXEbsESa8gtJnN3hyspq7EV4J6Fz3FjBk994r" , "rsaPrivateKeyXml" : "rsa-private-key.xml" , "rsaPublicKeyXml" : "rsa-public-key.xml" }

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; } } 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:

public class JWT { public string Token { get; set; } public long Expires { get; set; } } 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:

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 }; } } 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:

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); } 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:

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)); } } 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:

public class SignIn { public string Username { get; set; } public string Password { get; set; } } 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():

services.Configure<JwtSettings>(Configuration.GetSection("jwt")); services.AddSingleton<IJwtHandler,JwtHandler>(); 1 2 services . Configure < JwtSettings > ( Configuration . GetSection ( "jwt" ) ) ; services . AddSingleton < IJwtHandler , JwtHandler > ( ) ;

And move on to the Configure():

var jwtHandler = app.ApplicationServices.GetService<IJwtHandler>(); app.UseJwtBearerAuthentication(new JwtBearerOptions { AutomaticAuthenticate = true, TokenValidationParameters = jwtHandler.Parameters }); 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:

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" 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:

{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzcGV0eiIsInVuaXF1ZV9uYW1lIjoic3BldHoiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJpYXQiOjE1MDA4MTY3NjksIm5iZiI6MTUwMDgxNjc2OSwiZXhwIjoxNTAxMjQ4NzY5LCJqdGkiOiI4NzA0MWY1NzliN2M0NzFhYTVmMjgzY2Y0NzA0MmM0OCJ9.6wDNloIkii0gry98aMge4c73brFwTCbOV-YXcDpf8k0","expires":1501248769} 1 { "token" : "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzcGV0eiIsInVuaXF1ZV9uYW1lIjoic3BldHoiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJpYXQiOjE1MDA4MTY3NjksIm5iZiI6MTUwMDgxNjc2OSwiZXhwIjoxNTAxMjQ4NzY5LCJqdGkiOiI4NzA0MWY1NzliN2M0NzFhYTVmMjgzY2Y0NzA0MmM0OCJ9.6wDNloIkii0gry98aMge4c73brFwTCbOV-YXcDpf8k0" , "expires" : 1501248769 }

or

{"token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzcGV0eiIsInVuaXF1ZV9uYW1lIjoic3BldHoiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJpYXQiOjE1MDA4MTcwNDcsIm5iZiI6MTUwMDgxNzA0NywiZXhwIjoxNTAxMDc2MjQ3LCJqdGkiOiJkYjk1OWExYmUwMDc0ODk1YWMyMDdhZmRlYzM3OWM3YyJ9.KWAuNLlH2gwz1oujwSFKRAxbOpRhGy7jr5RvV4JH0DIhUBd2fTrHfC37_09B0dD3H3nqBWrrEdlyOODzeYyYcWym5pTsAPStgP9eIBrQH0fPubgrkkdLowAmnQWgwHy8Rqu7qkVx8kUP8zCFhmheVvlnlR87-pnZau1UJ_7bfsQuBNbiI6iAzp8TjjZXgvNYa_EwykQWr2kEgyYUkIUJhQgJvow7469l4vdTT27Ex3qSoN6IlBctS-LQPCIZvmxFT8kO5nWBtqeAqLsZmT63tdU2jfxKOweCn3n96UNJL78Y3cozRMtH_AQQb1MzAr_Dohur8PusHL7tyKQxCFZKXQ","expires":1501076247} 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.