Integrar autenticación Cognito con Floci local y soporte AWS
- Añade servicios y configuración Cognito para login web (admins) y API (scanners) - Soporta Floci cuando AWS_ENDPOINT_URL está definido; AWS real en caso contrario - Incluye bootstrap Floci, docker-compose.local.yml y carga automática en run-local.sh - Documenta arquitectura, flujos y arranque manual/IntelliJ en README - Actualiza gitignore: excluye Flutter, H2 local y artefactos generados
This commit is contained in:
@@ -27,6 +27,10 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>cognitoidentityprovider</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||
|
||||
+31
-58
@@ -1,77 +1,50 @@
|
||||
package mx.gob.slp.baseadminweb.configuration.security;
|
||||
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.JwsHeader;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class ApiAuthService {
|
||||
|
||||
private static final String SCANNER_ROLE = "ROLE_SCANNER";
|
||||
private final ObjectProvider<CognitoAuthService> cognitoAuthService;
|
||||
private final ObjectProvider<LocalJwtAuthService> localJwtAuthService;
|
||||
|
||||
private final AuthenticationManager authenticationManager;
|
||||
private final JwtEncoder jwtEncoder;
|
||||
private final JwtProperties jwtProperties;
|
||||
|
||||
public ApiAuthService(AuthenticationManager authenticationManager,
|
||||
JwtEncoder jwtEncoder,
|
||||
JwtProperties jwtProperties) {
|
||||
this.authenticationManager = authenticationManager;
|
||||
this.jwtEncoder = jwtEncoder;
|
||||
this.jwtProperties = jwtProperties;
|
||||
public ApiAuthService(ObjectProvider<CognitoAuthService> cognitoAuthService,
|
||||
ObjectProvider<LocalJwtAuthService> localJwtAuthService) {
|
||||
this.cognitoAuthService = cognitoAuthService;
|
||||
this.localJwtAuthService = localJwtAuthService;
|
||||
}
|
||||
|
||||
public ApiLoginResponse login(String username, String password) {
|
||||
Authentication authentication = authenticationManager.authenticate(
|
||||
new UsernamePasswordAuthenticationToken(username, password));
|
||||
|
||||
List<String> roles = authentication.getAuthorities().stream()
|
||||
.map(GrantedAuthority::getAuthority)
|
||||
.toList();
|
||||
|
||||
if (roles.stream().noneMatch(SCANNER_ROLE::equals)) {
|
||||
throw new BadCredentialsException("El usuario no tiene permisos para escanear codigos.");
|
||||
CognitoAuthService cognito = cognitoAuthService.getIfAvailable();
|
||||
if (cognito != null) {
|
||||
return cognito.login(username, password);
|
||||
}
|
||||
|
||||
Instant issuedAt = Instant.now();
|
||||
Instant expiresAt = issuedAt.plusSeconds(jwtProperties.getExpirationHours() * 3600L);
|
||||
LocalJwtAuthService local = localJwtAuthService.getIfAvailable();
|
||||
if (local != null) {
|
||||
return local.login(username, password);
|
||||
}
|
||||
|
||||
JwtClaimsSet claims = JwtClaimsSet.builder()
|
||||
.subject(authentication.getName())
|
||||
.issuedAt(issuedAt)
|
||||
.expiresAt(expiresAt)
|
||||
.claim("roles", roles)
|
||||
.build();
|
||||
|
||||
JwsHeader header = JwsHeader.with(MacAlgorithm.HS256).build();
|
||||
String accessToken = jwtEncoder.encode(JwtEncoderParameters.from(header, claims)).getTokenValue();
|
||||
|
||||
return new ApiLoginResponse(
|
||||
accessToken,
|
||||
"Bearer",
|
||||
jwtProperties.getExpirationHours() * 3600L,
|
||||
authentication.getName(),
|
||||
roles
|
||||
);
|
||||
throw new IllegalStateException("No hay proveedor de autenticacion API configurado.");
|
||||
}
|
||||
|
||||
public record ApiLoginResponse(
|
||||
String accessToken,
|
||||
String tokenType,
|
||||
long expiresIn,
|
||||
String username,
|
||||
List<String> roles
|
||||
) {
|
||||
public ApiLoginResponse refresh(String refreshToken) {
|
||||
if (refreshToken == null || refreshToken.isBlank()) {
|
||||
throw new BadCredentialsException("El refresh token es obligatorio.");
|
||||
}
|
||||
|
||||
CognitoAuthService cognito = cognitoAuthService.getIfAvailable();
|
||||
if (cognito != null) {
|
||||
return cognito.refresh(refreshToken);
|
||||
}
|
||||
|
||||
LocalJwtAuthService local = localJwtAuthService.getIfAvailable();
|
||||
if (local != null) {
|
||||
return local.refresh(refreshToken);
|
||||
}
|
||||
|
||||
throw new IllegalStateException("No hay proveedor de autenticacion API configurado.");
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package mx.gob.slp.baseadminweb.configuration.security;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record ApiLoginResponse(
|
||||
String accessToken,
|
||||
String refreshToken,
|
||||
String idToken,
|
||||
String tokenType,
|
||||
long expiresIn,
|
||||
String username,
|
||||
List<String> roles
|
||||
) {
|
||||
}
|
||||
+1
-1
@@ -27,7 +27,7 @@ public class ApiSecurityConfig {
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.authorizeHttpRequests(authorize -> authorize
|
||||
.requestMatchers("/api/auth/login").permitAll()
|
||||
.requestMatchers("/api/auth/login", "/api/auth/refresh").permitAll()
|
||||
.requestMatchers("/api/pass-codes/scan").hasRole("SCANNER")
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package mx.gob.slp.baseadminweb.configuration.security;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public record AuthenticatedWebUser(String username, String displayName) implements Serializable {
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package mx.gob.slp.baseadminweb.configuration.security;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@ConditionalOnProperty(prefix = "app.cognito", name = "enabled", havingValue = "true")
|
||||
public class CognitoAuthService {
|
||||
|
||||
private static final String SCANNER_ROLE = "ROLE_SCANNER";
|
||||
|
||||
private final CognitoUserAuthenticator cognitoUserAuthenticator;
|
||||
private final CognitoProperties cognitoProperties;
|
||||
|
||||
public CognitoAuthService(CognitoUserAuthenticator cognitoUserAuthenticator,
|
||||
CognitoProperties cognitoProperties) {
|
||||
this.cognitoUserAuthenticator = cognitoUserAuthenticator;
|
||||
this.cognitoProperties = cognitoProperties;
|
||||
}
|
||||
|
||||
public ApiLoginResponse login(String username, String password) {
|
||||
CognitoUserAuthenticator.AuthenticationResult result = cognitoUserAuthenticator.authenticate(
|
||||
username,
|
||||
password,
|
||||
cognitoProperties.getScannerClientId(),
|
||||
SCANNER_ROLE
|
||||
);
|
||||
return toLoginResponse(result);
|
||||
}
|
||||
|
||||
public ApiLoginResponse refresh(String refreshToken) {
|
||||
CognitoUserAuthenticator.AuthenticationResult result = cognitoUserAuthenticator.refresh(
|
||||
refreshToken,
|
||||
cognitoProperties.getScannerClientId(),
|
||||
SCANNER_ROLE
|
||||
);
|
||||
return toLoginResponse(result);
|
||||
}
|
||||
|
||||
private ApiLoginResponse toLoginResponse(CognitoUserAuthenticator.AuthenticationResult result) {
|
||||
var tokens = result.tokens();
|
||||
return new ApiLoginResponse(
|
||||
tokens.accessToken(),
|
||||
result.refreshToken(),
|
||||
tokens.idToken(),
|
||||
tokens.tokenType() != null ? tokens.tokenType() : "Bearer",
|
||||
tokens.expiresIn() != null ? tokens.expiresIn() : 0L,
|
||||
result.username(),
|
||||
result.roles()
|
||||
);
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package mx.gob.slp.baseadminweb.configuration.security;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
|
||||
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
|
||||
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
|
||||
import software.amazon.awssdk.regions.Region;
|
||||
import software.amazon.awssdk.services.cognitoidentityprovider.CognitoIdentityProviderClient;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
@Configuration
|
||||
@ConditionalOnProperty(prefix = "app.cognito", name = "enabled", havingValue = "true")
|
||||
public class CognitoClientConfig {
|
||||
|
||||
@Bean(destroyMethod = "close")
|
||||
CognitoIdentityProviderClient cognitoIdentityProviderClient(CognitoProperties properties) {
|
||||
var builder = CognitoIdentityProviderClient.builder()
|
||||
.region(Region.of(properties.getRegion()));
|
||||
|
||||
if (properties.usesLocalEndpoint()) {
|
||||
builder.endpointOverride(URI.create(properties.getEndpoint()))
|
||||
.credentialsProvider(StaticCredentialsProvider.create(
|
||||
AwsBasicCredentials.create("test", "test")));
|
||||
} else {
|
||||
builder.credentialsProvider(DefaultCredentialsProvider.create());
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package mx.gob.slp.baseadminweb.configuration.security;
|
||||
|
||||
import com.nimbusds.jwt.JWTParser;
|
||||
|
||||
final class CognitoDisplayNameResolver {
|
||||
|
||||
private CognitoDisplayNameResolver() {
|
||||
}
|
||||
|
||||
static String resolve(String idToken, String accessToken, String fallbackUsername) {
|
||||
String displayName = extractNameClaim(idToken);
|
||||
if (displayName == null || displayName.isBlank()) {
|
||||
displayName = extractNameClaim(accessToken);
|
||||
}
|
||||
if (displayName == null || displayName.isBlank()) {
|
||||
return fallbackUsername;
|
||||
}
|
||||
return displayName;
|
||||
}
|
||||
|
||||
private static String extractNameClaim(String token) {
|
||||
if (token == null || token.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JWTParser.parse(token).getJWTClaimsSet().getStringClaim("name");
|
||||
} catch (Exception ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
package mx.gob.slp.baseadminweb.configuration.security;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
@ConfigurationProperties(prefix = "app.cognito")
|
||||
public class CognitoProperties {
|
||||
|
||||
private boolean enabled;
|
||||
private String region = "us-east-1";
|
||||
private String userPoolId = "us-east-1_BaseAdminWeb";
|
||||
private String issuerUri = "http://localhost:4566/us-east-1_BaseAdminWeb";
|
||||
private String jwkSetUri = "http://localhost:4566/us-east-1_BaseAdminWeb/.well-known/jwks.json";
|
||||
private String endpoint;
|
||||
private String scannerClientId;
|
||||
private String adminClientId;
|
||||
private String scannerGroup = "scanners";
|
||||
private String adminGroup = "admins";
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public String getRegion() {
|
||||
return region;
|
||||
}
|
||||
|
||||
public void setRegion(String region) {
|
||||
this.region = region;
|
||||
}
|
||||
|
||||
public String getUserPoolId() {
|
||||
return userPoolId;
|
||||
}
|
||||
|
||||
public void setUserPoolId(String userPoolId) {
|
||||
this.userPoolId = userPoolId;
|
||||
}
|
||||
|
||||
public String getIssuerUri() {
|
||||
return issuerUri;
|
||||
}
|
||||
|
||||
public void setIssuerUri(String issuerUri) {
|
||||
this.issuerUri = issuerUri;
|
||||
}
|
||||
|
||||
public String getJwkSetUri() {
|
||||
return jwkSetUri;
|
||||
}
|
||||
|
||||
public void setJwkSetUri(String jwkSetUri) {
|
||||
this.jwkSetUri = jwkSetUri;
|
||||
}
|
||||
|
||||
public String getEndpoint() {
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
public void setEndpoint(String endpoint) {
|
||||
this.endpoint = endpoint;
|
||||
}
|
||||
|
||||
public boolean usesLocalEndpoint() {
|
||||
return endpoint != null && !endpoint.isBlank();
|
||||
}
|
||||
|
||||
public String getScannerClientId() {
|
||||
return scannerClientId;
|
||||
}
|
||||
|
||||
public void setScannerClientId(String scannerClientId) {
|
||||
this.scannerClientId = scannerClientId;
|
||||
}
|
||||
|
||||
public String getAdminClientId() {
|
||||
return adminClientId;
|
||||
}
|
||||
|
||||
public void setAdminClientId(String adminClientId) {
|
||||
this.adminClientId = adminClientId;
|
||||
}
|
||||
|
||||
public String getScannerGroup() {
|
||||
return scannerGroup;
|
||||
}
|
||||
|
||||
public void setScannerGroup(String scannerGroup) {
|
||||
this.scannerGroup = scannerGroup;
|
||||
}
|
||||
|
||||
public String getAdminGroup() {
|
||||
return adminGroup;
|
||||
}
|
||||
|
||||
public void setAdminGroup(String adminGroup) {
|
||||
this.adminGroup = adminGroup;
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package mx.gob.slp.baseadminweb.configuration.security;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
final class CognitoRoleMapper {
|
||||
|
||||
private CognitoRoleMapper() {
|
||||
}
|
||||
|
||||
static List<String> toRoles(Collection<String> groups, CognitoProperties properties) {
|
||||
if (groups == null || groups.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
List<String> roles = new ArrayList<>();
|
||||
for (String group : groups) {
|
||||
if (properties.getScannerGroup().equalsIgnoreCase(group)) {
|
||||
roles.add("ROLE_SCANNER");
|
||||
} else if (properties.getAdminGroup().equalsIgnoreCase(group)) {
|
||||
roles.add("ROLE_ADMIN");
|
||||
} else {
|
||||
roles.add("ROLE_" + group.toUpperCase());
|
||||
}
|
||||
}
|
||||
return roles;
|
||||
}
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
package mx.gob.slp.baseadminweb.configuration.security;
|
||||
|
||||
import com.nimbusds.jwt.JWTParser;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.stereotype.Component;
|
||||
import software.amazon.awssdk.services.cognitoidentityprovider.CognitoIdentityProviderClient;
|
||||
import software.amazon.awssdk.services.cognitoidentityprovider.model.AuthFlowType;
|
||||
import software.amazon.awssdk.services.cognitoidentityprovider.model.AuthenticationResultType;
|
||||
import software.amazon.awssdk.services.cognitoidentityprovider.model.InitiateAuthRequest;
|
||||
import software.amazon.awssdk.services.cognitoidentityprovider.model.InitiateAuthResponse;
|
||||
import software.amazon.awssdk.services.cognitoidentityprovider.model.NotAuthorizedException;
|
||||
import software.amazon.awssdk.services.cognitoidentityprovider.model.UserNotFoundException;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
@ConditionalOnProperty(prefix = "app.cognito", name = "enabled", havingValue = "true")
|
||||
public class CognitoUserAuthenticator {
|
||||
|
||||
private final CognitoIdentityProviderClient cognitoClient;
|
||||
private final CognitoProperties cognitoProperties;
|
||||
|
||||
public CognitoUserAuthenticator(CognitoIdentityProviderClient cognitoClient,
|
||||
CognitoProperties cognitoProperties) {
|
||||
this.cognitoClient = cognitoClient;
|
||||
this.cognitoProperties = cognitoProperties;
|
||||
}
|
||||
|
||||
public AuthenticationResult authenticate(String username,
|
||||
String password,
|
||||
String clientId,
|
||||
String requiredRole) {
|
||||
try {
|
||||
InitiateAuthResponse response = cognitoClient.initiateAuth(InitiateAuthRequest.builder()
|
||||
.authFlow(AuthFlowType.USER_PASSWORD_AUTH)
|
||||
.clientId(clientId)
|
||||
.authParameters(Map.of(
|
||||
"USERNAME", username,
|
||||
"PASSWORD", password
|
||||
))
|
||||
.build());
|
||||
|
||||
return toAuthenticationResult(response.authenticationResult(), username, requiredRole, null);
|
||||
} catch (NotAuthorizedException | UserNotFoundException ex) {
|
||||
throw new BadCredentialsException("Credenciales invalidas.");
|
||||
}
|
||||
}
|
||||
|
||||
public AuthenticationResult refresh(String refreshToken, String clientId, String requiredRole) {
|
||||
try {
|
||||
InitiateAuthResponse response = cognitoClient.initiateAuth(InitiateAuthRequest.builder()
|
||||
.authFlow(AuthFlowType.REFRESH_TOKEN_AUTH)
|
||||
.clientId(clientId)
|
||||
.authParameters(Map.of("REFRESH_TOKEN", refreshToken))
|
||||
.build());
|
||||
|
||||
return toAuthenticationResult(response.authenticationResult(), null, requiredRole, refreshToken);
|
||||
} catch (NotAuthorizedException ex) {
|
||||
throw new BadCredentialsException("Refresh token invalido o expirado.");
|
||||
}
|
||||
}
|
||||
|
||||
private AuthenticationResult toAuthenticationResult(AuthenticationResultType authResult,
|
||||
String fallbackUsername,
|
||||
String requiredRole,
|
||||
String fallbackRefreshToken) {
|
||||
if (authResult == null || authResult.accessToken() == null) {
|
||||
throw new BadCredentialsException("No se pudo autenticar con Cognito.");
|
||||
}
|
||||
|
||||
List<String> groups = extractGroups(authResult.accessToken());
|
||||
List<String> roles = CognitoRoleMapper.toRoles(groups, cognitoProperties);
|
||||
if (roles.stream().noneMatch(requiredRole::equals)) {
|
||||
throw new BadCredentialsException("El usuario no tiene permisos para acceder.");
|
||||
}
|
||||
|
||||
String username = extractUsername(authResult.accessToken(), fallbackUsername);
|
||||
String refreshToken = authResult.refreshToken() != null
|
||||
? authResult.refreshToken()
|
||||
: fallbackRefreshToken;
|
||||
|
||||
return new AuthenticationResult(authResult, username, roles, groups, refreshToken);
|
||||
}
|
||||
|
||||
private String extractUsername(String accessToken, String fallback) {
|
||||
try {
|
||||
var claims = JWTParser.parse(accessToken).getJWTClaimsSet();
|
||||
String username = claims.getStringClaim("username");
|
||||
if (username == null || username.isBlank()) {
|
||||
username = claims.getStringClaim("cognito:username");
|
||||
}
|
||||
return username != null && !username.isBlank() ? username : fallback;
|
||||
} catch (ParseException ex) {
|
||||
throw new BadCredentialsException("No se pudo leer el usuario del token.");
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> extractGroups(String accessToken) {
|
||||
try {
|
||||
var claims = JWTParser.parse(accessToken).getJWTClaimsSet();
|
||||
List<String> groups = claims.getStringListClaim("cognito:groups");
|
||||
return groups != null ? groups : List.of();
|
||||
} catch (ParseException ex) {
|
||||
throw new BadCredentialsException("No se pudieron leer los grupos del token.");
|
||||
}
|
||||
}
|
||||
|
||||
public record AuthenticationResult(
|
||||
AuthenticationResultType tokens,
|
||||
String username,
|
||||
List<String> roles,
|
||||
List<String> groups,
|
||||
String refreshToken
|
||||
) {
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
package mx.gob.slp.baseadminweb.configuration.security;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@ConditionalOnProperty(prefix = "app.cognito", name = "enabled", havingValue = "true")
|
||||
public class CognitoWebAuthenticationProvider implements AuthenticationProvider {
|
||||
|
||||
private static final String ADMIN_ROLE = "ROLE_ADMIN";
|
||||
|
||||
private final CognitoUserAuthenticator cognitoUserAuthenticator;
|
||||
private final CognitoProperties cognitoProperties;
|
||||
|
||||
public CognitoWebAuthenticationProvider(CognitoUserAuthenticator cognitoUserAuthenticator,
|
||||
CognitoProperties cognitoProperties) {
|
||||
this.cognitoUserAuthenticator = cognitoUserAuthenticator;
|
||||
this.cognitoProperties = cognitoProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
String username = authentication.getName();
|
||||
Object credentials = authentication.getCredentials();
|
||||
if (credentials == null) {
|
||||
throw new BadCredentialsException("La contrasena es obligatoria.");
|
||||
}
|
||||
|
||||
CognitoUserAuthenticator.AuthenticationResult result = cognitoUserAuthenticator.authenticate(
|
||||
username,
|
||||
credentials.toString(),
|
||||
cognitoProperties.getAdminClientId(),
|
||||
ADMIN_ROLE
|
||||
);
|
||||
|
||||
var authorities = result.roles().stream()
|
||||
.map(SimpleGrantedAuthority::new)
|
||||
.toList();
|
||||
|
||||
String displayName = CognitoDisplayNameResolver.resolve(
|
||||
result.tokens().idToken(),
|
||||
result.tokens().accessToken(),
|
||||
result.username()
|
||||
);
|
||||
|
||||
return UsernamePasswordAuthenticationToken.authenticated(
|
||||
new AuthenticatedWebUser(result.username(), displayName),
|
||||
null,
|
||||
authorities
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package mx.gob.slp.baseadminweb.configuration.security;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.ProviderManager;
|
||||
|
||||
@Configuration
|
||||
@ConditionalOnProperty(prefix = "app.cognito", name = "enabled", havingValue = "true")
|
||||
public class CognitoWebSecurityConfig {
|
||||
|
||||
@Bean(name = "webAuthenticationManager")
|
||||
AuthenticationManager webAuthenticationManager(CognitoWebAuthenticationProvider cognitoWebAuthenticationProvider) {
|
||||
return new ProviderManager(cognitoWebAuthenticationProvider);
|
||||
}
|
||||
}
|
||||
+30
-7
@@ -1,9 +1,13 @@
|
||||
package mx.gob.slp.baseadminweb.configuration.security;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
|
||||
@@ -19,12 +23,16 @@ import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(JwtProperties.class)
|
||||
@EnableConfigurationProperties({JwtProperties.class, CognitoProperties.class})
|
||||
public class JwtConfig {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = "app.cognito", name = "enabled", havingValue = "false")
|
||||
JwtEncoder jwtEncoder(JwtProperties jwtProperties) {
|
||||
OctetSequenceKey jwk = new OctetSequenceKey.Builder(jwtProperties.getSecret().getBytes(StandardCharsets.UTF_8))
|
||||
.algorithm(JWSAlgorithm.HS256)
|
||||
@@ -33,20 +41,35 @@ public class JwtConfig {
|
||||
}
|
||||
|
||||
@Bean
|
||||
JwtDecoder jwtDecoder(JwtProperties jwtProperties) {
|
||||
@ConditionalOnProperty(prefix = "app.cognito", name = "enabled", havingValue = "false")
|
||||
JwtDecoder localJwtDecoder(JwtProperties jwtProperties) {
|
||||
return NimbusJwtDecoder.withSecretKey(secretKey(jwtProperties))
|
||||
.macAlgorithm(MacAlgorithm.HS256)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
JwtAuthenticationConverter jwtAuthenticationConverter() {
|
||||
JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter();
|
||||
authoritiesConverter.setAuthoritiesClaimName("roles");
|
||||
authoritiesConverter.setAuthorityPrefix("");
|
||||
@ConditionalOnProperty(prefix = "app.cognito", name = "enabled", havingValue = "true")
|
||||
JwtDecoder cognitoJwtDecoder(CognitoProperties cognitoProperties) {
|
||||
return NimbusJwtDecoder.withJwkSetUri(cognitoProperties.getJwkSetUri()).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
JwtAuthenticationConverter jwtAuthenticationConverter(CognitoProperties cognitoProperties) {
|
||||
JwtGrantedAuthoritiesConverter localRolesConverter = new JwtGrantedAuthoritiesConverter();
|
||||
localRolesConverter.setAuthoritiesClaimName("roles");
|
||||
localRolesConverter.setAuthorityPrefix("");
|
||||
|
||||
JwtAuthenticationConverter authenticationConverter = new JwtAuthenticationConverter();
|
||||
authenticationConverter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
|
||||
authenticationConverter.setJwtGrantedAuthoritiesConverter(jwt -> {
|
||||
Collection<GrantedAuthority> authorities = new ArrayList<>();
|
||||
List<String> groups = jwt.getClaimAsStringList("cognito:groups");
|
||||
CognitoRoleMapper.toRoles(groups, cognitoProperties).stream()
|
||||
.map(SimpleGrantedAuthority::new)
|
||||
.forEach(authorities::add);
|
||||
authorities.addAll(localRolesConverter.convert(jwt));
|
||||
return authorities;
|
||||
});
|
||||
return authenticationConverter;
|
||||
}
|
||||
|
||||
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
package mx.gob.slp.baseadminweb.configuration.security;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.JwsHeader;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@ConditionalOnProperty(prefix = "app.cognito", name = "enabled", havingValue = "false")
|
||||
public class LocalJwtAuthService {
|
||||
|
||||
private static final String SCANNER_ROLE = "ROLE_SCANNER";
|
||||
|
||||
private final AuthenticationManager authenticationManager;
|
||||
private final JwtEncoder jwtEncoder;
|
||||
private final JwtProperties jwtProperties;
|
||||
|
||||
public LocalJwtAuthService(AuthenticationManager authenticationManager,
|
||||
JwtEncoder jwtEncoder,
|
||||
JwtProperties jwtProperties) {
|
||||
this.authenticationManager = authenticationManager;
|
||||
this.jwtEncoder = jwtEncoder;
|
||||
this.jwtProperties = jwtProperties;
|
||||
}
|
||||
|
||||
public ApiLoginResponse login(String username, String password) {
|
||||
Authentication authentication = authenticationManager.authenticate(
|
||||
new UsernamePasswordAuthenticationToken(username, password));
|
||||
|
||||
List<String> roles = authentication.getAuthorities().stream()
|
||||
.map(GrantedAuthority::getAuthority)
|
||||
.toList();
|
||||
|
||||
if (roles.stream().noneMatch(SCANNER_ROLE::equals)) {
|
||||
throw new BadCredentialsException("El usuario no tiene permisos para escanear codigos.");
|
||||
}
|
||||
|
||||
Instant issuedAt = Instant.now();
|
||||
Instant expiresAt = issuedAt.plusSeconds(jwtProperties.getExpirationHours() * 3600L);
|
||||
|
||||
JwtClaimsSet claims = JwtClaimsSet.builder()
|
||||
.subject(authentication.getName())
|
||||
.issuedAt(issuedAt)
|
||||
.expiresAt(expiresAt)
|
||||
.claim("roles", roles)
|
||||
.build();
|
||||
|
||||
JwsHeader header = JwsHeader.with(MacAlgorithm.HS256).build();
|
||||
String accessToken = jwtEncoder.encode(JwtEncoderParameters.from(header, claims)).getTokenValue();
|
||||
|
||||
return new ApiLoginResponse(
|
||||
accessToken,
|
||||
null,
|
||||
null,
|
||||
"Bearer",
|
||||
jwtProperties.getExpirationHours() * 3600L,
|
||||
authentication.getName(),
|
||||
roles
|
||||
);
|
||||
}
|
||||
|
||||
public ApiLoginResponse refresh(String refreshToken) {
|
||||
throw new BadCredentialsException("Refresh token no disponible sin Cognito.");
|
||||
}
|
||||
}
|
||||
+18
-3
@@ -1,10 +1,13 @@
|
||||
package mx.gob.slp.baseadminweb.configuration.security;
|
||||
|
||||
import mx.gob.slp.baseadminweb.application.service.AppUserDetailsService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||
@@ -22,12 +25,17 @@ public class SecurityConfig {
|
||||
|
||||
private final AppUserDetailsService appUserDetailsService;
|
||||
|
||||
@Autowired(required = false)
|
||||
@Qualifier("webAuthenticationManager")
|
||||
private AuthenticationManager webAuthenticationManager;
|
||||
|
||||
public SecurityConfig(AppUserDetailsService appUserDetailsService) {
|
||||
this.appUserDetailsService = appUserDetailsService;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http,
|
||||
PasswordEncoder passwordEncoder) throws Exception {
|
||||
http
|
||||
.securityMatcher(request -> !request.getRequestURI().startsWith("/api/"))
|
||||
.authorizeHttpRequests(authorize -> authorize
|
||||
@@ -49,9 +57,16 @@ public class SecurityConfig {
|
||||
.deleteCookies("JSESSIONID")
|
||||
)
|
||||
.headers(headers -> headers.frameOptions(frame -> frame.sameOrigin()))
|
||||
.userDetailsService(appUserDetailsService)
|
||||
.rememberMe(Customizer.withDefaults());
|
||||
|
||||
if (webAuthenticationManager != null) {
|
||||
http.authenticationManager(webAuthenticationManager);
|
||||
} else {
|
||||
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(appUserDetailsService);
|
||||
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder);
|
||||
http.authenticationProvider(daoAuthenticationProvider);
|
||||
}
|
||||
|
||||
http.csrf(csrf -> csrf.ignoringRequestMatchers("/h2-console/**"));
|
||||
return http.build();
|
||||
}
|
||||
@@ -65,4 +80,4 @@ public class SecurityConfig {
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user