Solo per mio divertimento voglio creare un Jwt Token che venga visto come valido su jwt.io.
Prima di ottenere questo faccio un passo indietro: come creare un jwt token in .net core. La prima decisione da prendere a riguardo è come poterlo firmare, e qui si può scegliere la strada della classica stringa alfanumerica che farà da chiave o l'uso di un certificato con chiave asimmetrica che permetterà l'utilizzo della chiave pubblica per verificarne la validità - ovviamente devo scegliere la seconda.
E' quindi giunto il momento di creare questo certificato. E' possibile usare anche comandi esterni come openssl, ma lascerò fare tutto a .net core:
var rsa = RSA.Create(); // generate asymmetric key pair var req = new CertificateRequest("cn=localhost", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); var cert = req.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(1)); // Create PFX (PKCS #12) with private key System.IO.File.WriteAllBytes("mycert.pfx", cert.Export(X509ContentType.Pfx, "P@55w0rd")); // Create Base 64 encoded CER (public key only) System.IO.File.WriteAllText("mycert.cer", "-----BEGIN CERTIFICATE-----\r\n" + Convert.ToBase64String(cert.Export(X509ContentType.Cert), Base64FormattingOptions.InsertLineBreaks) + "\r\n-----END CERTIFICATE-----");
Con il codice qui sopra vengono creati i due file, uno di tipo PKCS#12 che contiene la chiave privata e il file mycert.cer che contiene la chiave pubblica. Ora posso creare il Jwt Token super sicuro(!?!?):
static string GenerateToken() { var tokenHandler = new JwtSecurityTokenHandler(); var certificate = new X509Certificate2("mycert.pfx", "P@55w0rd"); var securityKey = new X509SecurityKey(certificate); var claims = new Dictionary<string, object>() { { ClaimTypes.Email, "a@a.it" }, { ClaimTypes.NameIdentifier, "az" } }; var tokenDescriptor = new SecurityTokenDescriptor { Claims = claims, Subject = new ClaimsIdentity(), Issuer = "Self", IssuedAt = DateTime.Now, Audience = "Others", Expires = DateTime.Now.AddYears(1), SigningCredentials = new SigningCredentials( securityKey, SecurityAlgorithms.RsaSha256Signature) }; var token = tokenHandler.CreateToken(tokenDescriptor); return tokenHandler.WriteToken(token); }
Che, eseguito, ritornerà il jwt token:
eyJhbGciOiJSUzI1NiIsImtpZCI6IkQwNTcwRkQwNEFDRjZBQzFGNEE5NkYxQUY5OUU1NzE5NTQ3Q0ZGNTAiLCJ0eXAiOiJKV1QifQ.eyJlbWFpbCI6ImFAYS5pdCIsIm5hbWVpZCI6ImF6IiwibmJmIjoxNjM1MzQ3MDE4LCJleHAiOjE2NjY4ODMwMTgsImlhdCI6MTYzNTM0NzAxOCwiaXNzIjoiU2VsZiIsImF1ZCI6Ik90aGVycyJ9.ahmGAI0dmVEcZaSBKs1Ff7ID-4r8-0_ITAXiifXyN-7T0_23_-njAea4ftcrf96dcrPW2Y8r1iv_TDAjZmqbn6EUbd-gGYJ1ggOWPEf44nQetJwUdkPKo_3MddlXcloRMaRcxK6YCxZ2uys-I0qqy-Y5RiewtOAzHetG7i3_VOr6KqtDKNb5oKYxtOQlovFxbMFp-DdsoHD3C5v8RPqdWlpBfvKVrZEb1XrJHJ2Mod6GYLQlE-zO_vrkt-MGb7ExEl1b34eACHGGjbm-FTVEZQEwPE5xqHa-8qdFRFRnGurnJpie4Mn0AHHep4loPlz5l-G6XQg3kL2IkBOSvWKkHw
In jwt.io:
Per poterlo validare da codice devo avere il file con la chiave pubblica (nel mio esempio mycert.cer), e scrivere questo codice:
static bool ValidateToken(string token) { var tokenHandler = new JwtSecurityTokenHandler(); byte[] certContent2 = System.IO.File.ReadAllBytes(@"mycert.cer"); var certificate = new X509Certificate2(certContent2); var securityKey = new X509SecurityKey(certificate); var validationParameters = new TokenValidationParameters { ValidAudience = "Others", ValidIssuer = "Self", IssuerSigningKey = securityKey }; var principal = tokenHandler.ValidateToken(token, validationParameters, out SecurityToken securityToken); if (principal == null) return false; if (securityToken == null) return false; return true; }
Questa funzione ritorna true se il certificato è valido. Ma il problema rimane, perché se controllo meglio in jwt.io la validità di questo token, vedrò:
Ovviamente non può essere validato perché jwt.io non sa dove prendersi la chiave pubblica e, sempre ovviamente, c'è il modo per dirgli dove prenderla e lo si può fare anche senza leggere una riga della documentazione ufficiale su questo tipo di token. E' sufficiente aprire la console di Chrome e vedere che cosa accade quando inserisco il mio token:
Viene fatta una chiamata ad un link come il seguente:
https://jwt.io/Self/.well-known/openid-configuration
In questo caso chiama se stesso ma solo perché nel mio token non è presente il dominio. Ma non è la sola chiamata che sarà fatta, perché dal link qui sopra sarà estratta come informazione l'url da dove prelevare la chiave pubblica. C'è una RFC a riguardo e se voglio fare in modo che il mio certificato sia validato, devo convertire il mio esempio in una web application che esponga quelle chiamate.
A questo link sono presenti la due web application di esempio, la prima per la creazione del jwt token, e la seconda è un'altra web application che, come da esempio, espone due API Rest di cui una pubblica e la seconda che risponderà solo nel caso il bearer token passato nell'header in Authorization sia valido. Ma prima di arrivare a tanto devo mettere a disposizione quell'url visto prima e lo faccio con questo codice:
[Route(".well-known/openid-configuration")] [ApiController] public class OpenIdController : ControllerBase { ... [EnableCors("mycors")] public IActionResult Get() { var obj = new OpenIdConfiguration { Issuer = _configuration.Issuer, // <- url di questa webapplication, http://localhost:40200 AuthorizationEndpoint = null, TokenEndpoint = null, DeviceAuthorizationEndpoint = null, UserinfoEndpoint = null, MfaChallengeEndpoint = null, JwksUri = _configuration.Issuer + ".well-known/jwks.json", RegistrationEndpoint = null, RevocationEndpoint = null, ScopesSupported = new[] { "openid", "profile" }, ResponseTypesSupported = new[] { "code", "token", "code token" }, CodeChallengeMethodsSupported = new[] { "S256", "plain" }, ResponseModesSupported = new[] { "query", "fragment", "form_post" }, SubjectTypesSupported = new[] { "public" }, IdTokenSigningAlgValuesSupported = new[] { "RS256" }, TokenEndpointAuthMethodsSupported = new[] { "client_secret_post" }, ClaimsSupported = new[] { "aud", "auth_time", "created_at", "email", "email_verified", "exp", "family_name", "given_name", "iat", "identities", "iss", "name", "nickname", "phone_number", "picture", "sub" }, RequestUriParameterSupported = false }; return Ok(obj); } }
Ovviamente queste informazioni le ho prese da una chiamata funzionante e molte di esse non sono utili per il risultato che voglio. A parte i valori statici inseriti solo due sono dinamici: Issuer e JwksUri; il primo è dove inserirò l'url (con solo il dominio) del server di autenticazione, e il secondo parametro è l'url dove prelevare le informazioni per validare il Jwt token.
Qui le informazioni non si possono copiare come sopra, e seguendo sempre la RFC, utilizzando come base il codice visto prima, prelevo altre informazioni dal certificato:
public CertificateInfo GenerateCertificate() { var rsa = RSA.Create(_configuration.RsaKeySize); // generate asymmetric key pair var req = new CertificateRequest("cn=localhost", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); var cert = req.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(1)); var securityKey = new X509SecurityKey(cert); string modulus = WebEncoders.Base64UrlEncode(rsa.ExportParameters(false).Modulus); string x5t = securityKey.X5t; string keyId = securityKey.KeyId; byte[] pfx = cert.Export(X509ContentType.Pfx, _configuration.PfxPassword); string publicCertficateBase64 = Convert.ToBase64String(cert.Export(X509ContentType.Cert), Base64FormattingOptions.None); return new CertificateInfo { Pfx = pfx, PublicCertificateBase64 = publicCertficateBase64, Modulus = modulus, X5t = x5t, KeyId = keyId }; }
Come da documentazione inserisco:
- Alg: il sistema di crittografazione usato.
- Kty: qui è la "famiglia" dell'algoritmo di crittografia utilizzato (normalmente per questo caso "RSA").
- Use: accetta solo due valori: sig (signature) e enc (encryption).
- N: in formato Base64urlUint è il valore del modulo per la chiave pubblica.
- E: valore dell'esponente per la chiave pubblica (viene sempre usata la stringa "AQAB").
- Kid: chiave utilizzata per proteggere la firma del token.
- X5t: è l'hash del certificato x509.
Quattro informazioni sono stringhe statiche, le restanti vengono prese durante la creazione del token, come visto sopra. Ora ho tutto per la creazione dell'API:
[Route(".well-known/jwks.json")] [ApiController] public class JwksController : ControllerBase { ... [EnableCors("mycors")] public async Task<IActionResult> GetAsync() { var certificateInfo = await _helper.GetCertificateInfoFromDiskAsync(); var obj = new KeyJwtClass { Keys = new[] { new JwksClass { Alg = "RS256", Kty = "RSA", Use = "sig", N = certificateInfo.Modulus, E = "AQAB", Kid= certificateInfo.KeyId, X5t = certificateInfo.X5t, X5c = new [] { certificateInfo.PublicCertificateBase64 } } } }; return Ok(obj); }
Dopo aver inserito una ulteriore API Rest per farmi restituire il token:
<a href="http://localhost:40200/api/token">http://localhost:40200/api/token</a>
Risposta:
eyJhbGciOiJSUzI1NiIsImtpZCI6IjYzRThDMDJCMkE1NjExN0Q0RUNENjQyRURBM0RDNkM1QjJBQTk5NjIiLCJ0eXAiOiJKV1QifQ.eyJlbWFpbCI6ImF4MkBheDIuaXQiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6InhhIiwic2NvcGUiOiJvcGVuaWQgcHJvZmlsZSByZWFkOm1lc3NhZ2VzIiwibmJmIjoxNjM1MzQ5NDk2LCJleHAiOjE2MzUzNDk3OTYsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDAyMDAvIiwiYXVkIjoiczZCaGRSa3F0MyJ9.21EzfTSrJxSyqL8ubk0IECBY88AeYZ4HF4HDAs6GfXDhDaYZWu1FeA8HH6GMOhJkUwY8iD5R5aCE_b3VXczsQKGR7ziB79GdLj88ouOfFxNogwKFYRrtgBOh2CH5KW2YL7MHSvXKC73dRjq9GvE5-B5doDPHEaRvfg19-kN58R3q0pzUFg11dXU6ikrPFwLTOgE-dpCIba1wIat1Xk2zhbEd6OXv-y7L2E8ewgJBltPdMfgXCkcuf_HEx8IFKOrIGSjj8sRseVIfudLruaNLq4RGq9EB-aI3oqOvDNc94q2DMyca_T8rPUkt22t1SaMpxeUUTgbEKD8-RL_cB6VX4w
E inserito in jwt.io:
Ha funzionato. E se faccio il controllo delle richieste di jwt.io vedo che sono effettuate le richieste alla mia webapplication che ha creato il token e rende disponibile gli endpoint con le informazioni per la sua verifica:
E l'utilità di tutto questo? Ora posso utilizzare questo token anche da altre applicazioni con il minimo sforzo e senza dover distribuire manualmente la chiave pubblica dove ne ho bisogno.
In Visual Studio creo la classica web api application e aggiungo queste poche righe di codice:
public void ConfigureServices(IServiceCollection services) { services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.Authority = $"http://{Configuration["Configuration:Domain"]}/"; options.Audience = Configuration["Configuration:Audience"]; options.IncludeErrorDetails = true; options.RequireHttpsMetadata = false; }); services.AddControllers(); }
I parametri sono solo due, il dominio del generatore di token e l'Audiance, che sarà utilizzato per verificare che il valore presente per quella chiave nel token sia valida per l'applicazione. Nel mio caso ho inserito una sequenza alfanumerica casuale, ma è possibile utilizzare anche il dominio della api che vogliamo utilizzare per esempio.
Infine aggiungo alla funzione Configure il controllo sull'autenticazione (quella sull'autorizzazione è già presente normalmente di default) - app.UseAuthentication() - e creo le due API Rest:
[Route("api")] [ApiController] public class ApiController : ControllerBase { [HttpGet("public")] public IActionResult Public() { return Ok(new { Message = "Hello from a public endpoint! You don't need to be authenticated to see this." }); } [HttpGet("private")] [Authorize] public IActionResult Private() { return Ok(new { Message = "Hello from a private endpoint! You need to be authenticated to see this." }); } }
Ovviamente la prima API sarà richiamabile anche da browser, la seconda da curl o Postman (o da codice) con il token visto prima senza il quale si avrà il classico errore 401.
curl --location --request GET 'http://localhost:40201/api/private' --header 'Authorization: Bearer [token]'
La comodità di tutto questo è che non ho dovuto far altro che aggiungere il modulo AddJwtBearer all'autenticazione e lui ha fatto tutto da solo: per la verifica del token ha richiamato in modo autonomo le api openid-configuration e jwks.json (l'esempio è preso da auth0).
Fine. Per ora basta, e se volessi creare una web application con tanto di identity server/authentication server per sherare l'autenticazione? Forse a breve...
Per inserire un commento, devi avere un account.
Fai il login e torna a questa pagina, oppure registrati alla nostra community.
- C# e Net 6 in Kubernetes con Prometheus e Grafana, il 12 gennaio 2022 alle 21:58
- Snaturare Kubernetes evitando i custom container Docker, il 6 gennaio 2022 alle 19:40
- Provando Kaniko in Kubernetes come alternativa a Docker per la creazione di immagini, il 18 dicembre 2021 alle 20:11
- Divertissement con l'OpenID e Access Token, il 6 dicembre 2021 alle 20:05
- Operator per Kubernetes in C# e Net Core 6., il 28 novembre 2021 alle 19:44
- RBAC in Kubernetes verso gli operator, il 21 novembre 2021 alle 20:52