Sotto il termine Microprofile si racchiude un insieme di standard che rappresentano un'alternativa leggera, tecnicamente parlando, a tutto ciò che si è sempre utilizzato in Java Enterprise. All'interno di questi standard troviamo la gestione di aspetti quali:
- Configuration management
- Health checks
- Fault tollerance
- Real-time performance metrics
- Distributed tracing
- Security
- OpenAPI support
- API client
In questo articolo analizzeremo JWT (JSON Web Token) su un Microservizio Quarkus per i servizi di autenticazione ed autorizzazione.
JSON Web Token
JSON Web Token è uno standard open source per i servizi di autenticazione ed autorizzazione che fornisce oggetti JSON firmati e/o cifrati per essere utilizzati come credenziali di autenticazione. Un flusso base di autenticazione JWT coinvolge i
seguenti step:
- un client si autentica normalmente in HTTPS con user e password verso un JWT provider;
- il provider JWT genera un token contenente claims, contenuti informativi utilizzati per verificare l'identità del chiamante;
- il client utilizzerà il token per autenticarsi verso ogni chiamata di metodi del microservizio.
In questo modo ci si autentica una sola volta fornendo username e password, ottenendo un token con una validità temporale da utilizzarsi per l'accesso al microservizio.
La specifica Microprofile definisce un requisito minimo di sicurezza per un token JWT che dovrebbe essere almeno firmato con RSASSA-PKCS-v1_5 utilizzando l'algoritmo SHA256 con una chiave a 2040 bit.
Configurazione Microprofile
In genere il generatore di token JWT è un servizio separato dedicato a questa funzionalità. Nel nostro caso, per mantenere la semplicità dell'esempio integriamo il servizio JWT all'interno del microservizio di conversione valuta visto in precedenza.
Evidenziamo che l'esempio ha come scopo l'introduzione all'uso di JWT, in uno scenario reale la comunicazione avviene in un contesto HTTPS e le chiavi non sono mantenute nei path applicativi.
Andiamo a generare preliminarmente un certificato a chiave pubblica e privata che utilizzeremo per firmare e cifrare il token JWT. Con riferimento a Linux, eseguiamo da terminale i comandi:
openssl genrsa -out basekey.pem 2048
openssl pkcs8 -topk8 -inform PEM -in basekey.pem -out privatekey.pem -nocrypt
openssl rsa -in basekey.pem -pubout privatekey.pem -outform PEM -out publickey.pem
Avendo come riferimento il progetto Intellij realizzato in precedenza, collochiamo i file privatekey.pem
e publickey.pem
rispettivamente nei percorsi META-INF
e META-INF/resources
. Aggiorniamo il file pom di Maven inserendo alcune dipendenze necessarie:
<dependency>
<roupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-jwt</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-jwt-build</artifactId>
<version>1.11.0.Final</version>
</dependency>
e aggiorniamo il file application.properties
per l'utilizzo del certificato a chiave pubblica:
mp.jwt.verify.publickey.location=META-INF/resources/publickey.pem
mp.jwt.verify.issuer=SupersonicSubatomic
I precedenti passaggi definiscono una configurazione Microprofile per il nostro progetto.
Aggiungiamo un JWT service provider al microservizio
Le porzioni di codice che seguono vanno inserite all'interno della classe CurrencyConverter
del precedente articolo. Partiamo con la definizione di un metodo per il caricamento e l'estrazione della chiave privata necessaria per cifrare il token:
private PrivateKey loadPrivateKey() throws IOException, InvalidKeySpecException, NoSuchAlgorithmException{
byte[] keyfile = CurrencyConverter.class.
getResourceAsStream("/META-INF/privatekey.pem").readAllBytes();
String key = new String(keyfile, 0, keyfile.length).
replaceAll("-----BEGIN (.*)-----","")
.replaceAll("-----END (.*)-----","")
.replaceAll("\r\n","")
.replaceAll("\n","").trim();
return KeyFactory.getInstance("RSA").
generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(key)));
}
proseguiamo realizzando il metodo per la generazione e cifratura (utilizzando la chiave privata) del token:
@ConfigProperty(name="mp.jwt.verify.issuer", defaultValue = "my-issuer-name")
String jwtIssuer;
private String generateJwt(String username, String password, List groups, Map roles)
throws InvalidKeySpecException, NoSuchAlgorithmException, IOException{
Map claimsMap = new HashMap();
claimsMap.put("username", username);
claimsMap.put("password", password);
JwtClaimsBuilder claims = Jwt.claims(claimsMap).
subject(username + " " + password).
claim("roleMAppings", roles).
claim("groups", groups).
issuer(jwtIssuer).
issuedAt(Instant.now().toEpochMilli()).
expiresAt(Instant.now().plus(2, ChronoUnit.DAYS).toEpochMilli());
PrivateKey privatekey = loadPrivateKey();
return claims.jws().sign(privatekey);
}
All'interno del metodo possiamo notare l'utilizzo di vari contenuti informativi inseriti come claims del token. Notiamo inoltre come al token sia stata assegnata una validità temporale.
Concludiamo il tutto con la realizzazione del metodo relativo all'endpoint rest JWT, chiamato dal client per la generazione del token:
@GET
@Path("{username}/{password}/jwt")
@Produces(MediaType.TEXT_PLAIN)
public Response getJWT(@PathParam(value = "username") String username, @PathParam(value = "username") String password) {
String jwt = null;
try {
Map rolesMap = new HashMap();
rolesMap.put("role 1", "ROLE1");
rolesMap.put("role 2", "ROLE2");
rolesMap.put("role 3", "ROLE3");
List groups = new ArrayList();
groups.add("Group 1");
jwt = generateJwt(username, password, groups, rolesMap);
} catch(Exception e) {
e.printStackTrace();
}
return Response.ok(jwt).build();
}
Rendere sicuro l'accesso al microservizio
Per rendere sicuro l'endpoint rest per la conversione di valuta, dobbiamo aggiungere, sui rispettivi metodi rest, l'annotation @RolesAllowed
specificando il ruolo consentito:
@GET
@Path("euro/{euro}")
@Produces(MediaType.APPLICATION_JSON)
@RolesAllowed("ROLE1")
public float dollarToEuro(@PathParam(value = "euro") float euro) {
Conversion conversion = new Conversion(euro * 1.19f, euro);
return conversion.getDollar();
}
@GET
@Path("dollar/{dollar}")
@Produces(MediaType.APPLICATION_JSON)
@RolesAllowed("ROLE1")
public float euroToDollar(@PathParam(value = "dollar") float dollar) {
Conversion conversion = new Conversion(dollar, dollar / 1.19f);
return conversion.getEuro();
}
e procedere con l'attivazione della sicurezza attraverso la definizione della classe:
package it.html;
import org.eclipse.microprofile.auth.LoginConfig;
import javax.ws.rs.core.Application;
@LoginConfig(authMethod = "MP-JWT", realmName = "TCK-MP-JWT")
public class AppConfig extends Application {
}
L'annotation LoginConfig
attiva la protezione JWT mentre i valori utilizzati per gli attributi authMethod
e realName
sono definiti dalla specifica Microprofile e devono essere impostati rispettivamente sui valori MP-JWT
e TCK-MP-JWT
.
Test del microservizio
Avviamo il microservizio con il classico quarkus:dev
all'interno di Intellij. Per richiamare l'endpoint del servizio utilizziamo l'applicativo Postman che rende molto semplice la costruzione di una chiamata HTTP.
Il primo test consiste nel verificare l'impossibilità di accedere al servizio in modo pubblico:
Perfetto, come mostrato in Figura 1 abbiamo verificato l'impossibilità di richiamare il servizio senza autenticazione. Accediamo quindi al servizio JWT per ottenere un token di accesso:
Copiamo il token restituito e costruiamo una chiamata HTTP che lo utilizzi come parametro header Authorization
:
Con soddisfazione osserviamo che questa volta l'accesso è consentito ed il servizio di conversione restituisce il valore atteso.