Buongiorno a tutti, sto cercando di far eseguire in locale una autenticazione tramite cas apereo, che si rifa' ad un db postgres gestito da backend java spring e frontend angular. Ho avviato il cas e va in stato ‘Ready’ ma non sono sicuro che le configurazioni del /cas/config.properties siano corrette.
Debuggando ho notato che il token JWT rimane sempre ‘INVALID’ e il frontend non riesce mai ad essere indirizzato verso la homepage di login.
Il punto in cui sembra rompersi e' il metodo validation.validate(….) sul try del metodo login(AuthData authData)
import java.io.UnsupportedEncodingException;
import java.net.ConnectException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.codec.binary.Base64;
import org.jasig.cas.client.validation.Assertion;
import org.jasig.cas.client.validation.Cas30ServiceTicketValidator;
import org.jasig.cas.client.validation.TicketValidationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.ExternalDocumentation;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
@Component
@AuthenticationNotRequired
@Path("/login")
@Tag(name="Sicurezza")
@PermitAll
public class LoginController {
private static final Logger log = LoggerFactory.getLogger(LoginController.class);
@Autowired
Cas30ServiceTicketValidator validator;
@Autowired
SecurityProperties globals;
@Autowired
JWTTokenService tokenService;
@Autowired
LoginService loginService;
@Autowired
PrfAgenziaService prfAgenziaService;
/*@Value("${development.authentication.bypass}")
private Boolean bypass;
@Value("${development.authentication.bypassusername}")
private String bypassUsername;
@Value("${development.authentication.bypassente}")
private String bypassEnte;
@Value("${development.authentication.bypassPermessi}")
private List<String> bypassPermessi;*/
@Autowired
SessionData sessionData;
@Autowired
PrfUtenteService prfUtenteService;
@Autowired
PrfUtenteAmministratoreService prfUtenteAmministratoreService;
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Path("/cas")
public Response cas(@Context HttpHeaders headers) {
try {
String base64Auth = headers.getHeaderString("authorization").replace("Basic ", "");
byte[] decodedBytes = Base64.decodeBase64(base64Auth);
String[] decodedCredentials = new String(decodedBytes, "UTF-8").split(":");
String username = decodedCredentials[0];
String password = decodedCredentials[1];
// Restituisco uno stato secondo la documentazione di CAS:
// https://apereo.github.io/cas/6.2.x/installation/Rest-Authentication.html
UserData data = loginService.login(username, password);
if (data == null)
return Response.status(Response.Status.NOT_FOUND).build();
// Risposta al CAS
CasAuthentication auth = new CasAuthentication();
auth.setId(username);
auth.getAttributes().put(globals.getCasAgenziaAttribute(), data.getAgenziaDataSourceName());
auth.getAttributes().put(globals.getCasPermessiAttribute(), String.join(",", data.getPermessi()));
log.info("LOGIN SUCCESS");
return Response.status(Response.Status.OK).entity(auth).build();
} catch (UnsupportedEncodingException e) {
log.error("Impossibile decodificare le credenziali inviate dal cas");
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
} catch (Exception ex) {
log.error(ex.getMessage());
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
}
}
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response login(AuthData authData) {
/*if (bypass) {
String jwttoken = tokenService.create(bypassUsername, bypassEnte, bypassPermessi);
AuthData tokenData = new AuthData();
tokenData.setUsername(bypassUsername);
tokenData.setServiceTicket(authData.getServiceTicket());
tokenData.setEnte(bypassEnte);
EnteDatabaseContextHolder.setEnteDatabase(bypassEnte);
tokenData.setPermessi(bypassPermessi);
tokenData.setJwtToken(jwttoken);
return Response.status(Status.OK).entity(tokenData).build();
}*/
try {
/*
* 1 - Verifico il token sul cas
*/
Assertion assertion = validator.validate(authData.getServiceTicket(), authData.getUrl()); //SI ROMPE QUI
/*
* 2 - Estraggo gli attributi del cas
*/
Map<String, Object> attributes = assertion.getPrincipal().getAttributes();
String casAgenziaAttribute = globals.getCasAgenziaAttribute();
String agenziaAttribute = (String) attributes.get(casAgenziaAttribute);
if (agenziaAttribute == null) {
throw new IllegalArgumentException(
"L'agenzia proveniente dal CAS non può essere nulla: popolare l'attributo");
}
String casPermessiAttribute = globals.getCasPermessiAttribute();
String permessiStringAttribute = (String) attributes.get(casPermessiAttribute);
if (permessiStringAttribute == null) {
throw new IllegalArgumentException(
"La lista dei permessi proveniente dal CAS non può essere nulla: popolare l'attributo (ad esempio permessi)");
}
List<String> permessiAttribute = permessiStringAttribute != null && !permessiStringAttribute.isBlank() ? Arrays.asList(permessiStringAttribute.split("\\s*,\\s*")) : new ArrayList<String>();
String serviceTicket = authData.getServiceTicket();
/*
* 3 - Genero il token JWT
*/
String jwttoken = tokenService.create(assertion.getPrincipal().getName(), agenziaAttribute, permessiAttribute);
/*
* 4 - Traccio la login
*/
tokenService.verifyAndSaveSession(jwttoken);
AgenziaDatabaseContextHolder.setAgenziaDatabase(sessionData.getAgenzia());
String username = sessionData.getUsername();
boolean fromAgenzia = sessionData.getAgenzia() != null;
boolean firstLogin = false;
if("true".equals(attributes.get("isFromNewLogin"))) {
//trackingEntitaService.insert("login", null, "login_successful", Map.of());
if(fromAgenzia) {
prfUtenteService.updateLogin(username);
} else {
prfUtenteAmministratoreService.updateLogin(username);
}
}
if (fromAgenzia) {
firstLogin = prfUtenteService.readByUsername(username).getCheckPassword() == 0;
} else {
firstLogin = prfUtenteAmministratoreService.readByUsername(username).getCheckPassword() == 0;
}
// Restituisco dati
AuthData tokenData = new AuthData();
tokenData.setUsername(assertion.getPrincipal().getName());
tokenData.setServiceTicket(serviceTicket);
tokenData.setAgenzia(agenziaAttribute);
tokenData.setPermessi(permessiAttribute);
tokenData.setJwtToken(jwttoken);
tokenData.setFirstLogin(firstLogin);
return Response.status(Status.OK).entity(tokenData).build();
} catch (TicketValidationException e) {
return Response.status(Status.UNAUTHORIZED)
.header("x-auth-redirect", globals.getCasUrlRedirect() + "?service=" + authData.getUrl()).build();
} catch (Throwable e1) {
if (e1.getCause() != null && e1.getCause() instanceof ConnectException) {
log.error("Impossibile raggiungere il server CAS ");
}
throw e1;
}
}
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Path("/token")
@Operation(summary = "Autenticazione",
externalDocs=@ExternalDocumentation(description="Esempio", url="../swagger-ui/examples/token.json"),
responses = {
@ApiResponse(responseCode = "200", content = @Content(mediaType = "application/json", schema = @Schema(implementation = AuthData.class))),
@ApiResponse(responseCode = "400", description = "Richiesta non valida"),
@ApiResponse(responseCode = "500", description = "Errore del server, contattare l'amministratore") })
public Response token(@Parameter(schema = @Schema(implementation = TokenRequest.class)) TokenRequest req) {
if(req.getClient_id() == null) {
throw new IllegalArgumentException("E' necessario specificare il client_id");
}
if(req.getClient_secret() == null) {
throw new IllegalArgumentException("E' necessario specificare il client_secret");
}
if(req.getGrant_type() == null || !"client_credentials".equals(req.getGrant_type())) {
throw new IllegalArgumentException("Il grant type supportato è client_credentials");
}
if(req.getScope() == null) {
throw new IllegalArgumentException("E' necessario specificare lo scope");
}
String ente = req.getScope().replace("access.ente.", "");
UserData userData = loginService.verificaClientCredentials(req.getClient_id(), req.getClient_secret(), ente);
String jwttoken = tokenService.create(req.getClient_id(), userData.getAgenziaDataSourceName(), userData.getPermessi());
Map<String, Object> response = new LinkedHashMap<String, Object>();
response.put("access_token", jwttoken);
response.put("expires_in", 9 * 3600);
response.put("token_type", "Bearer");
response.put("scope", req.getScope());
return Response.status(Status.OK).entity(response).build();
}
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Path("/token/utente")
@Operation(summary = "Autenticazione con username/password",
//externalDocs=@ExternalDocumentation(description="Esempio", url="../swagger-ui/examples/token.json"),
responses = {
@ApiResponse(responseCode = "200", content = @Content(mediaType = "application/json", schema = @Schema(implementation = AuthData.class))),
@ApiResponse(responseCode = "400", description = "Richiesta non valida"),
@ApiResponse(responseCode = "401", description = "Username o password non validi"),
@ApiResponse(responseCode = "500", description = "Errore del server, contattare l'amministratore") })
public Response tokenUtente(@Parameter(schema = @Schema(implementation = UserTokenRequest.class)) UserTokenRequest req) {
if(req.getUsername() == null) {
throw new IllegalArgumentException("E' necessario specificare lo username");
}
if(req.getPassword() == null) {
throw new IllegalArgumentException("E' necessario specificare la password");
}
UserData userData = null;
try {
userData = loginService.login(req.getUsername(), req.getPassword());
} catch (RuntimeException e) {
return Response.status(Response.Status.UNAUTHORIZED).entity(new HashMap<>()).type("application/json").build();
}
if (userData == null)
return Response.status(Response.Status.UNAUTHORIZED).entity(new HashMap<>()).type("application/json").build();
String jwttoken = tokenService.create(req.getUsername(), userData.getAgenziaDataSourceName(), userData.getPermessi());
Map<String, Object> response = new LinkedHashMap<String, Object>();
response.put("access_token", jwttoken);
response.put("expires_in", 9 * 3600);
response.put("token_type", "Bearer");
response.put("scope", userData.getAgenziaDataSourceName());
VPrfUtente utente = prfUtenteService.readByUsername(req.getUsername());
Map<String, String> resUser = new LinkedHashMap<String, String>();
resUser.put("username", utente.getNomeUtente());
resUser.put("nome", utente.getNome());
resUser.put("cognome", utente.getCognome());
resUser.put("mail", utente.getEmail());
resUser.put("agenzia", utente.getAgenziaDescrizione());
response.put("utente", resUser);
return Response.status(Status.OK).entity(response).build();
}
class CasAuthentication {
@JsonProperty("@class")
private final String classProperty;
private String id;
private final Map<String, Object> attributes;
public CasAuthentication() {
attributes = new HashMap<String, Object>();
attributes.put("@class", "java.util.Map<java.lang.String,java.lang.Object>");
classProperty = "org.apereo.cas.authentication.principal.SimplePrincipal";
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public Map<String, Object> getAttributes() {
return attributes;
}
}
}
il codice angular comprende due classi SecurityCheckComponent e AuthInterceptor :
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { DomSanitizer } from "@angular/platform-browser";
import { UserService } from "../user-service.service";
import { environment } from "../../../environments/environment";
import { UiService } from "src/app/modules/core/service/ui.service";
import { AuthenticationService } from "../authentication.service";
@Component({
selector: "app-security-check",
templateUrl: "./security-check.component.html",
styleUrls: ["./security-check.component.scss"],
})
export class SecurityCheckComponent implements OnInit {
constructor(
private userService: UserService,
private authenticationService: AuthenticationService,
private route: ActivatedRoute,
private ui: UiService,
private router: Router,
protected sanitizer: DomSanitizer
) {}
ngOnInit() {
this.ui.blockStart();
this.route.queryParams.subscribe((params) => {
console.log("params", params);
let authData: any = {
serviceTicket: params["ticket"],
url: params["url"],
};
// ESEGUIAMO SEMPRE LA LOGIN ARRIVATI A QUESTO PUNTO -> L'AUTH INTERCEPTOR È DELEGATO AL REDIRECT
this.authenticationService.login(authData).subscribe(
(userData: any) => {
// non è necessario gestire i 401 perchè già gestiti dall'auth interceptor
this.ui.blockStop();
this.userService.loadUserData(userData);
if (
!this.userService.permessi ||
this.userService.permessi.length == 0
) {
this.router.navigateByUrl("unauthorized");
return;
}
let urlPart = params.currentRoute.split("&")[0].split("?")[0];
let paramPart: string = params.currentRoute.split("?")[1];
paramPart = paramPart
.split("&")
.filter((param) => param.split("=")[0] != "ticket")
.join("&");
let url = urlPart + "?" + paramPart;
this.userService.loadUtente(() => {
this.router.navigateByUrl(url);
});
},
(error: any) => {
console.error(error);
}
);
});
}
}
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpErrorResponse, HttpResponse } from '@angular/common/http';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { UserService } from './user-service.service';
import { Router } from '@angular/router';
import { isArrayBuffer } from 'lodash';
import { UiService } from '../modules/core/service/ui.service';
import { Utils } from '../modules/core/utils/utils';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor(
private userService: UserService,
private router: Router,
private ui: UiService
) {
}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (req.headers.get("Authorization")) {
return next.handle(req);
}
let headers = this.userService.token ? req.headers.set('Authorization', 'Bearer ' + this.userService.token) : req.headers;
const authReq = req.clone({
headers
});
return next.handle(authReq).pipe(tap((event) => {
if (event instanceof HttpResponse) {
let token = event.headers.get('Token');
if (token && token.trim() != '') {
this.userService.token = token;
}
}
}, (err: any) => {
if (err instanceof HttpErrorResponse) {
this.ui.blockStop();
let error = err.error;
if(isArrayBuffer(err.error)){
let ab : ArrayBuffer = err.error;
error = Utils.arrayBufferToObj(ab);
}
if (err.status === 401) {
let casUrl = err.headers.get('x-auth-redirect');
if (!casUrl) {
return this.router.navigateByUrl('');
}
window.open(casUrl, '_self');
return;
}
if (err.status === 403) {
this.router.navigateByUrl("unauthorized");
return;
}
if (err.status === 400) {
this.ui.alertWarning(error.error);
}
if ([500, 402, 405, 410, 413].indexOf(err.status) != -1) {
console.log("CIAO SONO ENTRATO");
this.ui.alertError("Error nella comunicazione con il server: " + err.status + " - " + error.error);
}
return;
} else {
console.error("Error: ", err);
this.ui.blockStop();
}
return ['error'];
}));
}
}
la configurazione del cas e' composta nel seguente modo:
cas.server.name=http://localhost:8081
cas.server.prefix=${cas.server.name}/cas
server.port:8081
server.ssl.enabled=false
server.tomcat.protocol-header-https-value=http
cas.httpWebRequest.cors.enabled: true
cas.httpWebRequest.cors.allowCredentials=false
cas.httpWebRequest.cors.allowOrigins[0]=*
cas.httpWebRequest.cors.allowMethods[0]=*
cas.httpWebRequest.cors.allowHeaders[0]=*
logging.config=/opt/cas/config/log4j2.xml
cas.service-registry.core.init-from-json=false
cas.service-registry.json.location=file:/etc/cas/services-repo
cas.ticket.st.numberOfUses=1
cas.ticket.st.timeToKillInSeconds=3000
cas.logout.followServiceRedirects=true
cas.authn.accept.enabled=false
cas.authn.rest.uri=http://api:8080/api/login/cas
cas.authn.rest.password-encoder.encoding-algorithm=DEFAULT
cas.authn.rest.password-encoder.type=NONE
So che e' un domanda piu' ampia di quello che dovrebbe essere, e soprattutto con pochi elementi da analizzare ma magari mi sfugge una cosa stupida, o qualcuno con esperienza puo' accorgersi di qualcosa di lampante di cui io mi sono totalmente dimenticato.
Grazie :)