Configurazione Apereo Cas

di il
0 risposte

Configurazione Apereo Cas

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

Devi accedere o registrarti per scrivere nel forum
0 risposte