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
