diff --git a/src/main/java/it/smartcommunitylab/aac/api/ApiAuditController.java b/src/main/java/it/smartcommunitylab/aac/api/ApiAuditController.java index 020c1b8c4..f43b08dda 100644 --- a/src/main/java/it/smartcommunitylab/aac/api/ApiAuditController.java +++ b/src/main/java/it/smartcommunitylab/aac/api/ApiAuditController.java @@ -18,7 +18,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import it.smartcommunitylab.aac.api.scopes.ApiAuditScope; -import it.smartcommunitylab.aac.audit.BaseAuditController; +import it.smartcommunitylab.aac.audit.controller.BaseAuditController; import org.springframework.web.bind.annotation.RestController; @RestController diff --git a/src/main/java/it/smartcommunitylab/aac/audit/AuditManager.java b/src/main/java/it/smartcommunitylab/aac/audit/AuditManager.java index cd8d5421d..c5e593d5d 100644 --- a/src/main/java/it/smartcommunitylab/aac/audit/AuditManager.java +++ b/src/main/java/it/smartcommunitylab/aac/audit/AuditManager.java @@ -17,10 +17,13 @@ package it.smartcommunitylab.aac.audit; import it.smartcommunitylab.aac.Config; +import it.smartcommunitylab.aac.audit.model.ExtendedAuditEvent; +import it.smartcommunitylab.aac.audit.model.RealmAuditEvent; import it.smartcommunitylab.aac.audit.store.AuditEventStore; import java.time.Instant; import java.util.Date; import java.util.List; +import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -72,7 +75,7 @@ public long countPrincipalEvents(String realm, String principal, String type, Da return auditStore.countByPrincipal(principal, a, b, type); } - public List findRealmEvents(String realm, String type, Date after, Date before) { + public List findRealmEvents(String realm, String type, Date after, Date before) { Instant a = after == null ? null : after.toInstant(); Instant b = before == null ? null : before.toInstant(); diff --git a/src/main/java/it/smartcommunitylab/aac/audit/OAuth2EventListener.java b/src/main/java/it/smartcommunitylab/aac/audit/OAuth2EventListener.java index b1d92f902..e6404652c 100644 --- a/src/main/java/it/smartcommunitylab/aac/audit/OAuth2EventListener.java +++ b/src/main/java/it/smartcommunitylab/aac/audit/OAuth2EventListener.java @@ -96,13 +96,14 @@ private void onAuthorizationExceptionEvent(OAuth2AuthorizationExceptionEvent eve String type = "OAUTH2_" + errorCode.toUpperCase(); Map data = new HashMap<>(); + data.put("realm", realm); data.put("error", errorCode); data.put("summary", exception.getSummary()); data.put("message", exception.getMessage()); data.put("info", exception.getAdditionalInformation()); // build audit - RealmAuditEvent audit = new RealmAuditEvent(realm, Instant.now(), principal, type, data); + AuditEvent audit = new AuditEvent(Instant.now(), principal, type, data); // publish as event, listener will persist to store publish(audit); @@ -125,13 +126,14 @@ private void onTokenExceptionEvent(OAuth2TokenExceptionEvent event) { String type = "OAUTH2_" + errorCode.toUpperCase(); Map data = new HashMap<>(); + data.put("realm", realm); data.put("error", errorCode); data.put("summary", exception.getSummary()); data.put("message", exception.getMessage()); data.put("info", exception.getAdditionalInformation()); // build audit - RealmAuditEvent audit = new RealmAuditEvent(realm, Instant.now(), principal, type, data); + AuditEvent audit = new AuditEvent(Instant.now(), principal, type, data); // publish as event, listener will persist to store publish(audit); @@ -171,7 +173,7 @@ public void onTokenGrantEvent(TokenGrantEvent event) { } // build audit - RealmAuditEvent audit = new RealmAuditEvent(realm, Instant.now(), principal, TOKEN_GRANT, data); + AuditEvent audit = new AuditEvent(Instant.now(), principal, TOKEN_GRANT, data); // publish as event, listener will persist to store publish(audit); diff --git a/src/main/java/it/smartcommunitylab/aac/audit/RealmAuditEvent.java b/src/main/java/it/smartcommunitylab/aac/audit/RealmAuditEvent.java deleted file mode 100644 index 02838f1d4..000000000 --- a/src/main/java/it/smartcommunitylab/aac/audit/RealmAuditEvent.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2023 the original author or authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package it.smartcommunitylab.aac.audit; - -import it.smartcommunitylab.aac.SystemKeys; -import java.time.Instant; -import java.util.Map; -import org.springframework.boot.actuate.audit.AuditEvent; -import org.springframework.util.Assert; - -public class RealmAuditEvent extends AuditEvent { - - private static final long serialVersionUID = SystemKeys.AAC_CORE_SERIAL_VERSION; - - private final String realm; - - public RealmAuditEvent(String realm, Instant timestamp, String principal, String type, Map data) { - super(timestamp, principal, type, data); - Assert.notNull(realm, "realm can not be null"); - this.realm = realm; - } - - public String getRealm() { - return realm; - } - - @Override - public String toString() { - return ( - "RealmAuditEvent [realm=" + - realm + - ", timestamp=" + - getTimestamp() + - ", principal=" + - getPrincipal() + - ", type=" + - getType() + - ", data=" + - getData() + - "]" - ); - } - - public String getId() { - StringBuilder sb = new StringBuilder(); - sb.append(getTimestamp()); - if (getPrincipal() != null) { - sb.append("-").append(getPrincipal()); - } - return sb.toString(); - } - - public long getTime() { - return getTimestamp() != null ? getTimestamp().getEpochSecond() * 1000 : -1; - } -} diff --git a/src/main/java/it/smartcommunitylab/aac/audit/BaseAuditController.java b/src/main/java/it/smartcommunitylab/aac/audit/controller/BaseAuditController.java similarity index 91% rename from src/main/java/it/smartcommunitylab/aac/audit/BaseAuditController.java rename to src/main/java/it/smartcommunitylab/aac/audit/controller/BaseAuditController.java index 487b67aca..65ac71a49 100644 --- a/src/main/java/it/smartcommunitylab/aac/audit/BaseAuditController.java +++ b/src/main/java/it/smartcommunitylab/aac/audit/controller/BaseAuditController.java @@ -14,11 +14,12 @@ * limitations under the License. */ -package it.smartcommunitylab.aac.audit; +package it.smartcommunitylab.aac.audit.controller; import io.swagger.v3.oas.annotations.Operation; import it.smartcommunitylab.aac.Config; import it.smartcommunitylab.aac.SystemKeys; +import it.smartcommunitylab.aac.audit.AuditManager; import it.smartcommunitylab.aac.common.NoSuchRealmException; import java.util.Collection; import java.util.Date; @@ -30,6 +31,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.audit.AuditEvent; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.util.Assert; @@ -65,7 +67,7 @@ public String getAuthority() { @GetMapping("/audit/{realm}") @Operation(summary = "find audit events from a given realm") - public Collection findEvents( + public Collection findEvents( @PathVariable @Valid @NotNull @Pattern(regexp = SystemKeys.SLUG_PATTERN) String realm, @RequestParam(required = false, name = "type") Optional type, @RequestParam(required = false, name = "after") @DateTimeFormat( @@ -75,7 +77,7 @@ public Collection findEvents( iso = DateTimeFormat.ISO.DATE_TIME ) Optional before ) throws NoSuchRealmException { - logger.debug("find audit events for realm [}", StringUtils.trimAllWhitespace(realm)); + logger.debug("find audit events for realm {}", StringUtils.trimAllWhitespace(realm)); return auditManager.findRealmEvents(realm, type.orElse(null), after.orElse(null), before.orElse(null)); } diff --git a/src/main/java/it/smartcommunitylab/aac/audit/AuthorizationEventListener.java b/src/main/java/it/smartcommunitylab/aac/audit/listeners/AuthorizationEventListener.java similarity index 81% rename from src/main/java/it/smartcommunitylab/aac/audit/AuthorizationEventListener.java rename to src/main/java/it/smartcommunitylab/aac/audit/listeners/AuthorizationEventListener.java index a6d8c7cc3..5628c779a 100644 --- a/src/main/java/it/smartcommunitylab/aac/audit/AuthorizationEventListener.java +++ b/src/main/java/it/smartcommunitylab/aac/audit/listeners/AuthorizationEventListener.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package it.smartcommunitylab.aac.audit; +package it.smartcommunitylab.aac.audit.listeners; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,13 +35,12 @@ public void onApplicationEvent(AbstractAuthorizationEvent event) { // each request without authentication triggers an unauthorized event which ends // in store, we don't want those in db try { - if (event instanceof AuthorizationFailureEvent) { - AuthorizationFailureEvent failureEvent = (AuthorizationFailureEvent) event; - - if (logger.isTraceEnabled()) { - failureEvent.getAccessDeniedException().printStackTrace(); - } - } + // if (event instanceof AuthorizationFailureEvent) { + // AuthorizationFailureEvent failureEvent = (AuthorizationFailureEvent) event; + // // if (logger.isTraceEnabled()) { + // // failureEvent.getAccessDeniedException().printStackTrace(); + // // } + // } logger.trace("authorization event " + event.toString()); } catch (Exception e) { diff --git a/src/main/java/it/smartcommunitylab/aac/audit/listeners/ClientAuthenticationEventListener.java b/src/main/java/it/smartcommunitylab/aac/audit/listeners/ClientAuthenticationEventListener.java new file mode 100644 index 000000000..b89557514 --- /dev/null +++ b/src/main/java/it/smartcommunitylab/aac/audit/listeners/ClientAuthenticationEventListener.java @@ -0,0 +1,97 @@ +/* + * Copyright 2023 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package it.smartcommunitylab.aac.audit.listeners; + +import it.smartcommunitylab.aac.core.auth.ClientAuthentication; +import java.util.HashMap; +import java.util.Map; +import org.springframework.boot.actuate.audit.AuditEvent; +import org.springframework.boot.actuate.security.AbstractAuthenticationAuditListener; +import org.springframework.security.authentication.event.AbstractAuthenticationEvent; +import org.springframework.security.authentication.event.AbstractAuthenticationFailureEvent; +import org.springframework.security.authentication.event.AuthenticationSuccessEvent; +import org.springframework.security.core.AuthenticationException; + +public class ClientAuthenticationEventListener extends AbstractAuthenticationAuditListener { + + public static final String CLIENT_AUTHENTICATION_FAILURE = "CLIENT_AUTHENTICATION_FAILURE"; + public static final String CLIENT_AUTHENTICATION_SUCCESS = "CLIENT_AUTHENTICATION_SUCCESS"; + + @Override + public void onApplicationEvent(AbstractAuthenticationEvent event) { + if (event instanceof AuthenticationSuccessEvent) { + onAuthenticationSuccessEvent((AuthenticationSuccessEvent) event); + } else if (event instanceof AbstractAuthenticationFailureEvent) { + onAuthenticationFailureEvent((AbstractAuthenticationFailureEvent) event); + } + } + + private void onAuthenticationFailureEvent(AbstractAuthenticationFailureEvent event) { + AuthenticationException ex = event.getException(); + if (!(event.getAuthentication() instanceof ClientAuthentication)) { + return; + } + + ClientAuthentication auth = (ClientAuthentication) event.getAuthentication(); + String principal = auth.getPrincipal(); + Object details = auth.getDetails(); + String eventType = CLIENT_AUTHENTICATION_FAILURE; + + // build data + Map data = new HashMap<>(); + data.put("type", ex.getClass().getName()); + data.put("message", ex.getMessage()); + data.put("auth", auth.getClass().getName()); + + // persist details, should be safe to store + if (details != null) { + data.put("details", details); + } + + // build audit + AuditEvent audit = new AuditEvent(principal, eventType, data); + + // publish as event, listener will persist to store + publish(audit); + } + + private void onAuthenticationSuccessEvent(AuthenticationSuccessEvent event) { + if (!(event.getAuthentication() instanceof ClientAuthentication)) { + return; + } + + ClientAuthentication auth = (ClientAuthentication) event.getAuthentication(); + String principal = auth.getName(); + Object details = auth.getDetails(); + String eventType = CLIENT_AUTHENTICATION_SUCCESS; + + Map data = new HashMap<>(); + data.put("auth", auth.getClass().getName()); + data.put("realm", auth.getRealm()); + + // persist details, should be safe to store + if (details != null) { + data.put("details", details); + } + + // build audit + AuditEvent audit = new AuditEvent(principal, eventType, data); + + // publish as event, listener will persist to store + publish(audit); + } +} diff --git a/src/main/java/it/smartcommunitylab/aac/audit/AuthenticationEventListener.java b/src/main/java/it/smartcommunitylab/aac/audit/listeners/UserAuthenticationEventListener.java similarity index 51% rename from src/main/java/it/smartcommunitylab/aac/audit/AuthenticationEventListener.java rename to src/main/java/it/smartcommunitylab/aac/audit/listeners/UserAuthenticationEventListener.java index 8e7544855..b7fac8b3e 100644 --- a/src/main/java/it/smartcommunitylab/aac/audit/AuthenticationEventListener.java +++ b/src/main/java/it/smartcommunitylab/aac/audit/listeners/UserAuthenticationEventListener.java @@ -14,42 +14,33 @@ * limitations under the License. */ -package it.smartcommunitylab.aac.audit; +package it.smartcommunitylab.aac.audit.listeners; import it.smartcommunitylab.aac.SystemKeys; -import it.smartcommunitylab.aac.core.auth.ClientAuthentication; import it.smartcommunitylab.aac.core.auth.UserAuthentication; import it.smartcommunitylab.aac.core.auth.WrappedAuthenticationToken; +import it.smartcommunitylab.aac.events.UserAuthenticationFailureEvent; +import it.smartcommunitylab.aac.events.UserAuthenticationSuccessEvent; import it.smartcommunitylab.aac.identity.model.ConfigurableIdentityProvider; import it.smartcommunitylab.aac.identity.service.IdentityProviderService; import java.time.Instant; import java.util.HashMap; import java.util.Map; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.boot.actuate.audit.AuditEvent; import org.springframework.boot.actuate.security.AbstractAuthenticationAuditListener; import org.springframework.security.authentication.event.AbstractAuthenticationEvent; -import org.springframework.security.authentication.event.AbstractAuthenticationFailureEvent; -import org.springframework.security.authentication.event.AuthenticationSuccessEvent; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; -import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; import org.springframework.util.StringUtils; -public class AuthenticationEventListener extends AbstractAuthenticationAuditListener { +public class UserAuthenticationEventListener extends AbstractAuthenticationAuditListener { - private final Logger logger = LoggerFactory.getLogger(getClass()); - - public static final String AUTHENTICATION_SUCCESS = "AUTHENTICATION_SUCCESS"; - public static final String AUTHENTICATION_FAILURE = "AUTHENTICATION_FAILURE"; + // public static final String AUTHENTICATION_SUCCESS = "AUTHENTICATION_SUCCESS"; + // public static final String AUTHENTICATION_FAILURE = "AUTHENTICATION_FAILURE"; public static final String USER_AUTHENTICATION_FAILURE = "USER_AUTHENTICATION_FAILURE"; public static final String USER_AUTHENTICATION_SUCCESS = "USER_AUTHENTICATION_SUCCESS"; - public static final String CLIENT_AUTHENTICATION_FAILURE = "CLIENT_AUTHENTICATION_FAILURE"; - public static final String CLIENT_AUTHENTICATION_SUCCESS = "CLIENT_AUTHENTICATION_SUCCESS"; - //TODO replace with identity authority provider service to read only *active* providers private IdentityProviderService providerService; @@ -63,10 +54,10 @@ public void onApplicationEvent(AbstractAuthenticationEvent event) { onUserAuthenticationFailureEvent((UserAuthenticationFailureEvent) event); } else if (event instanceof UserAuthenticationSuccessEvent) { onUserAuthenticationSuccessEvent((UserAuthenticationSuccessEvent) event); - } else if (event instanceof AuthenticationSuccessEvent) { - onAuthenticationSuccessEvent((AuthenticationSuccessEvent) event); - } else if (event instanceof AbstractAuthenticationFailureEvent) { - onAuthenticationFailureEvent((AbstractAuthenticationFailureEvent) event); + // } else if (event instanceof AuthenticationSuccessEvent) { + // onAuthenticationSuccessEvent((AuthenticationSuccessEvent) event); + // } else if (event instanceof AbstractAuthenticationFailureEvent) { + // onAuthenticationFailureEvent((AbstractAuthenticationFailureEvent) event); } } @@ -97,33 +88,33 @@ private void onUserAuthenticationFailureEvent(UserAuthenticationFailureEvent eve data.put("authority", authority); data.put("provider", provider); data.put("realm", realm); - data.put("type", ex.getClass().getSimpleName()); data.put("message", ex.getMessage()); + if (authentication instanceof WrappedAuthenticationToken) { + // persist web details, should be safe to store + data.put("details", ((WrappedAuthenticationToken) authentication).getAuthenticationDetails()); + } + if (SystemKeys.EVENTS_LEVEL_DETAILS.equals(level) || SystemKeys.EVENTS_LEVEL_FULL.equals(level)) { data.put("exception", event.exportException()); - - if (authentication instanceof WrappedAuthenticationToken) { - // persist web details, should be safe to store - data.put("details", ((WrappedAuthenticationToken) authentication).getAuthenticationDetails()); - } } if (SystemKeys.EVENTS_LEVEL_FULL.equals(level)) { // persist full authentication token // export to ensure we can serialize the token data.put("authentication", event.exportAuthentication()); + //TODO store full event } // build audit - RealmAuditEvent audit = new RealmAuditEvent(realm, Instant.now(), principal, eventType, data); + AuditEvent audit = new AuditEvent(Instant.now(), principal, eventType, data); // publish as event, listener will persist to store publish(audit); } private void onUserAuthenticationSuccessEvent(UserAuthenticationSuccessEvent event) { - UserAuthentication auth = event.getAuthenticationToken(); + UserAuthentication auth = event.getUserAuthentication(); String principal = auth.getSubjectId(); String authority = event.getAuthority(); String provider = event.getProvider(); @@ -148,11 +139,9 @@ private void onUserAuthenticationSuccessEvent(UserAuthenticationSuccessEvent eve data.put("provider", provider); data.put("realm", realm); - if (SystemKeys.EVENTS_LEVEL_DETAILS.equals(level) || SystemKeys.EVENTS_LEVEL_FULL.equals(level)) { - // persist web details, should be safe to store - if (auth.getWebAuthenticationDetails() != null) { - data.put("details", auth.getWebAuthenticationDetails()); - } + // persist web details, should be safe to store + if (auth.getWebAuthenticationDetails() != null) { + data.put("details", auth.getWebAuthenticationDetails()); } if (SystemKeys.EVENTS_LEVEL_FULL.equals(level)) { @@ -161,94 +150,95 @@ private void onUserAuthenticationSuccessEvent(UserAuthenticationSuccessEvent eve // make sure credentials are cleared from this context auth.eraseCredentials(); data.put("authentication", auth); - } + //TODO store full event - // build audit - RealmAuditEvent audit = new RealmAuditEvent(realm, Instant.now(), principal, eventType, data); - - // publish as event, listener will persist to store - publish(audit); - } - - private void onAuthenticationFailureEvent(AbstractAuthenticationFailureEvent event) { - AuthenticationException ex = event.getException(); - Authentication auth = event.getAuthentication(); - String principal = ""; - Object details = auth.getDetails(); - String eventType = AUTHENTICATION_FAILURE; - - // try to extract details if wrapped - if (auth instanceof WrappedAuthenticationToken) { - WrappedAuthenticationToken token = (WrappedAuthenticationToken) auth; - eventType = USER_AUTHENTICATION_FAILURE; - auth = token.getAuthenticationToken(); - principal = auth.getName(); - details = token.getAuthenticationDetails(); - } - - if (auth instanceof ClientAuthentication) { - ClientAuthentication token = (ClientAuthentication) auth; - eventType = CLIENT_AUTHENTICATION_FAILURE; - details = token.getWebAuthenticationDetails(); - } - - if (auth instanceof BearerTokenAuthenticationToken) { - // principal is token, we ignore for now - // we could store in data to support reuse detection etc - // but JWT are large (>4k) and expensive - principal = ""; - } - - // build data - Map data = new HashMap<>(); - data.put("type", ex.getClass().getName()); - data.put("message", ex.getMessage()); - data.put("auth", auth.getClass().getName()); - - // persist details, should be safe to store - if (details != null) { - data.put("details", details); - } - - // build audit - AuditEvent audit = new AuditEvent(principal, eventType, data); - - // publish as event, listener will persist to store - publish(audit); - } - - private void onAuthenticationSuccessEvent(AuthenticationSuccessEvent event) { - Authentication auth = event.getAuthentication(); - String principal = auth.getName(); - Object details = auth.getDetails(); - String eventType = AUTHENTICATION_SUCCESS; - - Map data = new HashMap<>(); - data.put("auth", auth.getClass().getName()); - - // persist details, should be safe to store - if (details != null) { - data.put("details", details); - } - - // check if user auth - if (auth instanceof UserAuthentication) { - UserAuthentication token = (UserAuthentication) auth; - eventType = USER_AUTHENTICATION_SUCCESS; - data.put("realm", token.getRealm()); - // TODO get last provider from tokens, needs ordering or dedicated field - } - if (auth instanceof ClientAuthentication) { - ClientAuthentication token = (ClientAuthentication) auth; - eventType = CLIENT_AUTHENTICATION_SUCCESS; - details = token.getWebAuthenticationDetails(); - data.put("realm", token.getRealm()); } // build audit - AuditEvent audit = new AuditEvent(principal, eventType, data); + AuditEvent audit = new AuditEvent(Instant.now(), principal, eventType, data); // publish as event, listener will persist to store publish(audit); } + // private void onAuthenticationFailureEvent(AbstractAuthenticationFailureEvent event) { + // AuthenticationException ex = event.getException(); + // Authentication auth = event.getAuthentication(); + // String principal = ""; + // Object details = auth.getDetails(); + // String eventType = AUTHENTICATION_FAILURE; + + // // try to extract details if wrapped + // if (auth instanceof WrappedAuthenticationToken) { + // WrappedAuthenticationToken token = (WrappedAuthenticationToken) auth; + // eventType = USER_AUTHENTICATION_FAILURE; + // auth = token.getAuthenticationToken(); + // principal = auth.getName(); + // details = token.getAuthenticationDetails(); + // } + + // if (auth instanceof ClientAuthentication) { + // ClientAuthentication token = (ClientAuthentication) auth; + // eventType = CLIENT_AUTHENTICATION_FAILURE; + // details = token.getWebAuthenticationDetails(); + // } + + // if (auth instanceof BearerTokenAuthenticationToken) { + // // principal is token, we ignore for now + // // we could store in data to support reuse detection etc + // // but JWT are large (>4k) and expensive + // principal = ""; + // } + + // // build data + // Map data = new HashMap<>(); + // data.put("type", ex.getClass().getName()); + // data.put("message", ex.getMessage()); + // data.put("auth", auth.getClass().getName()); + + // // persist details, should be safe to store + // if (details != null) { + // data.put("details", details); + // } + + // // build audit + // AuditEvent audit = new AuditEvent(principal, eventType, data); + + // // publish as event, listener will persist to store + // publish(audit); + // } + + // private void onAuthenticationSuccessEvent(AuthenticationSuccessEvent event) { + // Authentication auth = event.getAuthentication(); + // String principal = auth.getName(); + // Object details = auth.getDetails(); + // String eventType = AUTHENTICATION_SUCCESS; + + // Map data = new HashMap<>(); + // data.put("auth", auth.getClass().getName()); + + // // persist details, should be safe to store + // if (details != null) { + // data.put("details", details); + // } + + // // check if user auth + // if (auth instanceof UserAuthentication) { + // UserAuthentication token = (UserAuthentication) auth; + // eventType = USER_AUTHENTICATION_SUCCESS; + // data.put("realm", token.getRealm()); + // // TODO get last provider from tokens, needs ordering or dedicated field + // } + // if (auth instanceof ClientAuthentication) { + // ClientAuthentication token = (ClientAuthentication) auth; + // eventType = CLIENT_AUTHENTICATION_SUCCESS; + // details = token.getWebAuthenticationDetails(); + // data.put("realm", token.getRealm()); + // } + + // // build audit + // AuditEvent audit = new AuditEvent(principal, eventType, data); + + // // publish as event, listener will persist to store + // publish(audit); + // } } diff --git a/src/main/java/it/smartcommunitylab/aac/audit/model/ApplicationAuditEvent.java b/src/main/java/it/smartcommunitylab/aac/audit/model/ApplicationAuditEvent.java new file mode 100644 index 000000000..a0b2d8125 --- /dev/null +++ b/src/main/java/it/smartcommunitylab/aac/audit/model/ApplicationAuditEvent.java @@ -0,0 +1,76 @@ +/* + * Copyright 2023 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package it.smartcommunitylab.aac.audit.model; + +import java.util.Map; +import org.springframework.context.ApplicationEvent; +import org.springframework.security.oauth2.core.ClaimAccessor; + +public interface ApplicationAuditEvent extends ClaimAccessor { + public static final String EVENT_KEY = "event"; + + default E getEvent() { + E event = null; + + if (getClaims() != null && getClaims().containsKey(EVENT_KEY)) { + //try cast and handle error + try { + @SuppressWarnings("unchecked") + E e = (E) getClaims().get(EVENT_KEY); + event = e; + } catch (ClassCastException e) { + throw new IllegalArgumentException("invalid event"); + } + } + + return event; + } + + default String getClazz() { + Map map = this.getClaimAsMap(EVENT_KEY); + if (map == null || !map.containsKey("@class")) { + return null; + } + + //try cast and handle error + String value = null; + try { + String c = (String) map.get("@class"); + value = c; + } catch (ClassCastException e) { + throw new IllegalArgumentException("invalid value for clazz"); + } + + return value; + } + // public static ApplicationAuditEvent from(AuditEvent audit, Class clazz) { + // ApplicationAuditEvent ea = new ApplicationAuditEvent<>( + // audit.getTimestamp(), + // audit.getPrincipal(), + // audit.getType(), + // audit.getData() + // ); + + // try { + // if (ea.getClazz() != null && clazz.isAssignableFrom(Class.forName(ea.getClazz()))) { + // return ea; + // } + // } catch (ClassNotFoundException e) {} + + // throw new IllegalArgumentException("invalid or missing event"); + // } +} diff --git a/src/main/java/it/smartcommunitylab/aac/audit/model/ExtendedAuditEvent.java b/src/main/java/it/smartcommunitylab/aac/audit/model/ExtendedAuditEvent.java new file mode 100644 index 000000000..40cde326f --- /dev/null +++ b/src/main/java/it/smartcommunitylab/aac/audit/model/ExtendedAuditEvent.java @@ -0,0 +1,63 @@ +/* + * Copyright 2023 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package it.smartcommunitylab.aac.audit.model; + +import it.smartcommunitylab.aac.SystemKeys; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import org.springframework.boot.actuate.audit.AuditEvent; +import org.springframework.context.ApplicationEvent; + +public class ExtendedAuditEvent + extends AuditEvent + implements ApplicationAuditEvent, TxAuditEvent, RealmAuditEvent { + + private static final long serialVersionUID = SystemKeys.AAC_CORE_SERIAL_VERSION; + + protected ExtendedAuditEvent(Instant timestamp, String principal, String type, Map data) { + super(timestamp, principal, type, data); + } + + static Map buildData(Map initialData, String[] keys, Object[] values) { + Map data = new HashMap<>(); + if (initialData != null) { + data.putAll(initialData); + } + + if (keys != null && values != null) { + if (keys.length != values.length) { + throw new IllegalArgumentException("invalid number of parameters"); + } + + for (int i = 0; i < keys.length; i++) { + data.put(keys[i], values[i]); + } + } + + return data; + } + + @Override + public Map getClaims() { + return getData(); + } + + public static ExtendedAuditEvent from(AuditEvent event) { + return new ExtendedAuditEvent<>(event.getTimestamp(), event.getPrincipal(), event.getType(), event.getData()); + } +} diff --git a/src/main/java/it/smartcommunitylab/aac/audit/model/RealmAuditEvent.java b/src/main/java/it/smartcommunitylab/aac/audit/model/RealmAuditEvent.java new file mode 100644 index 000000000..b5e3e07d9 --- /dev/null +++ b/src/main/java/it/smartcommunitylab/aac/audit/model/RealmAuditEvent.java @@ -0,0 +1,27 @@ +/* + * Copyright 2023 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package it.smartcommunitylab.aac.audit.model; + +import org.springframework.security.oauth2.core.ClaimAccessor; + +public interface RealmAuditEvent extends ClaimAccessor { + public static final String REALM_KEY = "realm"; + + default String getRealm() { + return getClaimAsString(REALM_KEY); + } +} diff --git a/src/main/java/it/smartcommunitylab/aac/audit/model/TxAuditEvent.java b/src/main/java/it/smartcommunitylab/aac/audit/model/TxAuditEvent.java new file mode 100644 index 000000000..aa8a858c2 --- /dev/null +++ b/src/main/java/it/smartcommunitylab/aac/audit/model/TxAuditEvent.java @@ -0,0 +1,27 @@ +/* + * Copyright 2023 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package it.smartcommunitylab.aac.audit.model; + +import org.springframework.security.oauth2.core.ClaimAccessor; + +public interface TxAuditEvent extends ClaimAccessor { + public static final String TX_KEY = "tx"; + + default String getTx() { + return getClaimAsString(TX_KEY); + } +} diff --git a/src/main/java/it/smartcommunitylab/aac/audit/store/AuditApplicationEventMixIns.java b/src/main/java/it/smartcommunitylab/aac/audit/store/AuditApplicationEventMixIns.java new file mode 100644 index 000000000..1a972917c --- /dev/null +++ b/src/main/java/it/smartcommunitylab/aac/audit/store/AuditApplicationEventMixIns.java @@ -0,0 +1,22 @@ +/** + * Copyright 2024 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package it.smartcommunitylab.aac.audit.store; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "@class") +public final class AuditApplicationEventMixIns {} diff --git a/src/main/java/it/smartcommunitylab/aac/audit/store/AuditEventStore.java b/src/main/java/it/smartcommunitylab/aac/audit/store/AuditEventStore.java index 6fcac5bf6..bf1a009b0 100644 --- a/src/main/java/it/smartcommunitylab/aac/audit/store/AuditEventStore.java +++ b/src/main/java/it/smartcommunitylab/aac/audit/store/AuditEventStore.java @@ -16,7 +16,6 @@ package it.smartcommunitylab.aac.audit.store; -import it.smartcommunitylab.aac.audit.RealmAuditEvent; import java.time.Instant; import java.util.List; import org.springframework.boot.actuate.audit.AuditEvent; @@ -25,9 +24,13 @@ public interface AuditEventStore extends AuditEventRepository { public long countByRealm(String realm, Instant after, Instant before, String type); + public long countByTx(String tx, String type); + public long countByPrincipal(String principal, Instant after, Instant before, String type); - public List findByRealm(String realm, Instant after, Instant before, String type); + public List findByRealm(String realm, Instant after, Instant before, String type); + + public List findByTx(String realm, String type); public List findByPrincipal(String principal, Instant after, Instant before, String type); } diff --git a/src/main/java/it/smartcommunitylab/aac/audit/store/AutoJdbcAuditEventStore.java b/src/main/java/it/smartcommunitylab/aac/audit/store/AutoJdbcAuditEventStore.java index c7d460037..09128b6c7 100644 --- a/src/main/java/it/smartcommunitylab/aac/audit/store/AutoJdbcAuditEventStore.java +++ b/src/main/java/it/smartcommunitylab/aac/audit/store/AutoJdbcAuditEventStore.java @@ -16,39 +16,64 @@ package it.smartcommunitylab.aac.audit.store; -import it.smartcommunitylab.aac.audit.RealmAuditEvent; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.cbor.databind.CBORMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import it.smartcommunitylab.aac.audit.model.ApplicationAuditEvent; +import it.smartcommunitylab.aac.audit.model.ExtendedAuditEvent; +import it.smartcommunitylab.aac.audit.model.RealmAuditEvent; +import it.smartcommunitylab.aac.audit.model.TxAuditEvent; +import java.io.IOException; import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Timestamp; import java.sql.Types; import java.time.Instant; +import java.util.Collections; +import java.util.HashMap; import java.util.LinkedList; import java.util.List; -import java.util.stream.Collectors; +import java.util.Map; import javax.sql.DataSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.boot.actuate.audit.AuditEvent; +import org.springframework.context.ApplicationEvent; +import org.springframework.core.convert.converter.Converter; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.support.SqlLobValue; -import org.springframework.security.oauth2.common.util.SerializationUtils; import org.springframework.util.Assert; import org.springframework.util.StringUtils; public class AutoJdbcAuditEventStore implements AuditEventStore { + private final Logger logger = LoggerFactory.getLogger(getClass()); + private final TypeReference> typeRef = new TypeReference>() {}; + private final JdbcTemplate jdbcTemplate; - private final RowMapper rowMapper = new AuditEventRowMapper(); + private RowMapper rowMapper; + private ObjectMapper mapper; private static final String DEFAULT_INSERT_STATEMENT = - "INSERT INTO audit_events (event_time, principal, realm , event_type, event_data ) VALUES (?, ?, ?, ?, ?)"; + "INSERT INTO audit_events (event_time, principal, realm, tx, event_type, event_class, event_data ) VALUES (?, ?, ?, ?, ?, ?, ?)"; private static final String DEFAULT_SELECT_PRINCIPAL_STATEMENT = - "SELECT event_time, principal, realm, event_type, event_data FROM audit_events WHERE principal = ?"; + "SELECT event_time, principal, event_type, event_data FROM audit_events WHERE principal = ?"; private static final String DEFAULT_SELECT_REALM_STATEMENT = - "SELECT event_time, principal, realm, event_type, event_data FROM audit_events WHERE realm = ?"; + "SELECT event_time, principal, event_type, event_data FROM audit_events WHERE realm = ?"; + private static final String DEFAULT_SELECT_TX_STATEMENT = + "SELECT event_time, principal, event_type, event_data FROM audit_events WHERE tx = ?"; private static final String DEFAULT_COUNT_PRINCIPAL_STATEMENT = "SELECT COUNT(*) FROM audit_events WHERE principal = ?"; private static final String DEFAULT_COUNT_REALM_STATEMENT = "SELECT COUNT(*) FROM audit_events WHERE realm = ?"; + private static final String DEFAULT_COUNT_TX_STATEMENT = "SELECT COUNT(*) FROM audit_events WHERE tx = ?"; private static final String TIME_AFTER_CONDITION = "event_time >= ?"; private static final String TIME_BETWEEN_CONDITION = "event_time BETWEEN ? AND ? "; @@ -60,8 +85,10 @@ public class AutoJdbcAuditEventStore implements AuditEventStore { private String selectByPrincipalAuditEvent = DEFAULT_SELECT_PRINCIPAL_STATEMENT; private String selectByRealmAuditEvent = DEFAULT_SELECT_REALM_STATEMENT; + private String selectByTxAuditEvent = DEFAULT_SELECT_TX_STATEMENT; private String countByPrincipalAuditEvent = DEFAULT_COUNT_PRINCIPAL_STATEMENT; private String countByRealmAuditEvent = DEFAULT_COUNT_REALM_STATEMENT; + private String countByTxAuditEvent = DEFAULT_COUNT_TX_STATEMENT; private String timeAfterCondition = TIME_AFTER_CONDITION; private String timeBetweenCondition = TIME_BETWEEN_CONDITION; @@ -69,32 +96,84 @@ public class AutoJdbcAuditEventStore implements AuditEventStore { private String orderBy = DEFAULT_ORDER_BY; + private Converter, byte[]> writer; + private Converter> reader; + public AutoJdbcAuditEventStore(DataSource dataSource) { Assert.notNull(dataSource, "DataSource required"); this.jdbcTemplate = new JdbcTemplate(dataSource); + + //use CBOR by default as mapper + this.mapper = + new CBORMapper() + .registerModule(new JavaTimeModule()) + //include only non-null fields + .setSerializationInclusion(Include.NON_NULL) + //add mixin for including typeInfo in events + .addMixIn(ApplicationEvent.class, AuditApplicationEventMixIns.class); + + this.writer = + map -> { + try { + return (mapper.writeValueAsBytes(map)); + } catch (JsonProcessingException e) { + logger.error("error writing data: {}", e.getMessage()); + throw new IllegalArgumentException("error writing data", e); + } + }; + + this.reader = + bytes -> { + try { + return mapper.readValue(bytes, typeRef); + } catch (IOException e) { + logger.error("error reading data: {}", e.getMessage()); + throw new IllegalArgumentException("error reading data", e); + } + }; + + this.rowMapper = new AuditEventMappedRowMapper(reader); + } + + public void setWriter(Converter, byte[]> writer) { + this.writer = writer; + } + + public void setReader(Converter> reader) { + this.reader = reader; + this.rowMapper = new AuditEventMappedRowMapper(reader); } @Override public void add(AuditEvent event) { - // extract data + // extract data and repack String principal = event.getPrincipal(); long time = event.getTimestamp().toEpochMilli(); String type = event.getType(); - String realm = null; - if (event instanceof RealmAuditEvent) { - realm = ((RealmAuditEvent) event).getRealm(); - } + + ExtendedAuditEvent eae = ExtendedAuditEvent.from(event); + + String realm = eae.getRealm(); + String tx = eae.getTx(); + String clazz = eae.getClazz(); + + //pack audit event in data + Map data = mapper.convertValue(event, typeRef); + + byte[] bytes = writer != null ? writer.convert(data) : null; jdbcTemplate.update( insertAuditEventSql, - new Object[] { - new java.sql.Timestamp(time), - principal, - realm, - type, - new SqlLobValue(SerializationUtils.serialize(event)), - }, - new int[] { Types.TIMESTAMP, Types.VARCHAR, Types.VARCHAR, Types.VARCHAR, Types.BLOB } + new Object[] { new java.sql.Timestamp(time), principal, realm, tx, type, clazz, new SqlLobValue(bytes) }, + new int[] { + Types.TIMESTAMP, + Types.VARCHAR, + Types.VARCHAR, + Types.VARCHAR, + Types.VARCHAR, + Types.VARCHAR, + Types.BLOB, + } ); } @@ -153,7 +232,7 @@ public long countByRealm(String realm, Instant after, Instant before, String typ } @Override - public List findByRealm(String realm, Instant after, Instant before, String type) { + public List findByRealm(String realm, Instant after, Instant before, String type) { StringBuilder query = new StringBuilder(); query.append(selectByRealmAuditEvent); @@ -178,12 +257,45 @@ public List findByRealm(String realm, Instant after, Instant be query.append(" ").append(orderBy); - return jdbcTemplate - .query(query.toString(), rowMapper, params.toArray(new Object[0])) - .stream() - .filter(e -> (e instanceof RealmAuditEvent)) - .map(e -> (RealmAuditEvent) e) - .collect(Collectors.toList()); + return jdbcTemplate.query(query.toString(), rowMapper, params.toArray(new Object[0])); + } + + @Override + public long countByTx(String tx, String type) { + StringBuilder query = new StringBuilder(); + query.append(countByTxAuditEvent); + + List params = new LinkedList<>(); + params.add(tx); + + if (StringUtils.hasText(type)) { + query.append(" AND ").append(typeCondition); + params.add(type); + } + + Long count = jdbcTemplate.queryForObject(query.toString(), Long.class, params.toArray(new Object[0])); + if (count == null) { + return 0; + } + return count.longValue(); + } + + @Override + public List findByTx(String tx, String type) { + StringBuilder query = new StringBuilder(); + query.append(selectByTxAuditEvent); + + List params = new LinkedList<>(); + params.add(tx); + + if (StringUtils.hasText(type)) { + query.append(" AND ").append(typeCondition); + params.add(type); + } + + query.append(" ").append(orderBy); + + return jdbcTemplate.query(query.toString(), rowMapper, params.toArray(new Object[0])); } @Override @@ -282,16 +394,35 @@ public void setOrderBy(String orderBy) { this.orderBy = orderBy; } - private static class AuditEventRowMapper implements RowMapper { + private class AuditEventMappedRowMapper implements RowMapper { + + private final Converter> reader; + + public AuditEventMappedRowMapper(Converter> reader) { + this.reader = reader; + } @Override public AuditEvent mapRow(ResultSet rs, int rowNum) throws SQLException { - // long time = rs.getLong("time"); - // String principal = rs.getString("principal"); - // String realm = rs.getString("realm"); - // String type = rs.getString("type"); - AuditEvent event = SerializationUtils.deserialize(rs.getBytes("event_data")); - return event; + Timestamp time = rs.getTimestamp("event_time"); + String principal = rs.getString("principal"); + String type = rs.getString("event_type"); + byte[] bytes = rs.getBytes("event_data"); + Map data = Collections.emptyMap(); + Map raw = reader != null ? reader.convert(bytes) : Collections.emptyMap(); + + //unpack event to extract data + if (raw != null && raw.containsKey("data")) { + try { + @SuppressWarnings("unchecked") + Map d = (Map) raw.get("data"); + data = d; + } catch (ClassCastException e) { + throw new IllegalArgumentException("invalid value for data"); + } + } + + return new AuditEvent(time.toInstant(), principal, type, data); } } } diff --git a/src/main/java/it/smartcommunitylab/aac/audit/store/SignedAuditDataReader.java b/src/main/java/it/smartcommunitylab/aac/audit/store/SignedAuditDataReader.java new file mode 100644 index 000000000..f0d6809e4 --- /dev/null +++ b/src/main/java/it/smartcommunitylab/aac/audit/store/SignedAuditDataReader.java @@ -0,0 +1,74 @@ +/** + * Copyright 2024 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package it.smartcommunitylab.aac.audit.store; + +import com.nimbusds.jose.jwk.JWKSet; +import it.smartcommunitylab.aac.jwt.JwtDecoderBuilder; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtException; +import org.springframework.security.oauth2.jwt.JwtValidators; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; +import org.springframework.util.Assert; + +public class SignedAuditDataReader implements Converter> { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private final String issuer; + private final JWKSet jwks; + + private NimbusJwtDecoder decoder; + + public SignedAuditDataReader(String issuer, JWKSet jwks) { + Assert.hasText(issuer, "issuer is required"); + Assert.notNull(jwks, "jwks can not be null"); + + this.issuer = issuer; + this.jwks = jwks; + + this.decoder = new JwtDecoderBuilder().jwks(jwks).build(); + this.decoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuer)); + } + + public void setSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) { + //rebuild decoder + this.decoder = new JwtDecoderBuilder().jwks(jwks).jwsAlgorithm(signatureAlgorithm).build(); + this.decoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuer)); + } + + @Override + public Map convert(byte[] source) { + if (this.decoder == null) { + throw new RuntimeException("invalid or missing configuration"); + } + + try { + String token = new String(source, StandardCharsets.UTF_8); + Jwt jwt = decoder.decode(token); + return jwt.getClaims(); + } catch (JwtException e) { + logger.error("error converting the map with the provided configuration: {}", e.getMessage()); + throw new IllegalArgumentException("error converting the map with the provided configuration", e); + } + } +} diff --git a/src/main/java/it/smartcommunitylab/aac/audit/store/SignedAuditDataWriter.java b/src/main/java/it/smartcommunitylab/aac/audit/store/SignedAuditDataWriter.java new file mode 100644 index 000000000..9f66187c3 --- /dev/null +++ b/src/main/java/it/smartcommunitylab/aac/audit/store/SignedAuditDataWriter.java @@ -0,0 +1,131 @@ +/** + * Copyright 2024 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package it.smartcommunitylab.aac.audit.store; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.crypto.ECDSASigner; +import com.nimbusds.jose.crypto.MACSigner; +import com.nimbusds.jose.crypto.RSASSASigner; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.KeyType; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.Date; +import java.util.Map; +import java.util.UUID; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.oauth2.jose.jws.MacAlgorithm; +import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; +import org.springframework.util.Assert; + +public class SignedAuditDataWriter implements Converter, byte[]> { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private final String issuer; + private final JWK jwk; + + private JWSSigner signer; + private JWSAlgorithm jwsAlgorithm; + + public SignedAuditDataWriter(String issuer, JWK jwk) { + Assert.hasText(issuer, "issuer is required"); + Assert.notNull(jwk, "jwk can not be null"); + + this.issuer = issuer; + this.jwk = jwk; + + try { + this.jwsAlgorithm = resolveAlgorithm(jwk); + this.signer = buildJwsSigner(jwk); + } catch (JOSEException e) { + logger.error("error processing key {}", e.getMessage()); + } + + if (signer == null || jwsAlgorithm == null) { + logger.error("error building signer from key"); + throw new IllegalArgumentException("error building signer from key"); + } + } + + @Override + public byte[] convert(Map source) { + if (this.signer == null || jwsAlgorithm == null) { + throw new RuntimeException("invalid or missing configuration"); + } + + try { + JWSHeader header = new JWSHeader.Builder(jwsAlgorithm).keyID(jwk.getKeyID()).build(); + Instant issuedAt = Instant.now(); + + JWTClaimsSet.Builder claims = new JWTClaimsSet.Builder() + .issuer(issuer) + .jwtID(UUID.randomUUID().toString()) + .issueTime(Date.from(issuedAt)); + + if (source != null) { + source.entrySet().forEach(e -> claims.claim(e.getKey(), e.getValue())); + } + + SignedJWT jwt = new SignedJWT(header, claims.build()); + jwt.sign(signer); + + return jwt.serialize().getBytes(StandardCharsets.UTF_8); + } catch (JOSEException e) { + logger.error("error converting the map with the provided configuration: {}", e.getMessage()); + throw new IllegalArgumentException("error converting the map with the provided configuration", e); + } + } + + private static JWSSigner buildJwsSigner(JWK jwk) throws JOSEException { + if (KeyType.RSA.equals(jwk.getKeyType())) { + return new RSASSASigner(jwk.toRSAKey()); + } else if (KeyType.EC.equals(jwk.getKeyType())) { + return new ECDSASigner(jwk.toECKey()); + } else if (KeyType.OCT.equals(jwk.getKeyType())) { + return new MACSigner(jwk.toOctetSequenceKey()); + } + + return null; + } + + private static JWSAlgorithm resolveAlgorithm(JWK jwk) { + String jwsAlgorithm = null; + if (jwk.getAlgorithm() != null) { + jwsAlgorithm = jwk.getAlgorithm().getName(); + } + + if (jwsAlgorithm == null) { + if (KeyType.RSA.equals(jwk.getKeyType())) { + jwsAlgorithm = SignatureAlgorithm.RS256.getName(); + } else if (KeyType.EC.equals(jwk.getKeyType())) { + jwsAlgorithm = SignatureAlgorithm.ES256.getName(); + } else if (KeyType.OCT.equals(jwk.getKeyType())) { + jwsAlgorithm = MacAlgorithm.HS256.getName(); + } + } + + return jwsAlgorithm == null ? null : JWSAlgorithm.parse(jwsAlgorithm); + } +} diff --git a/src/main/java/it/smartcommunitylab/aac/config/AuditConfig.java b/src/main/java/it/smartcommunitylab/aac/config/AuditConfig.java index be726f45e..0f4dc41cc 100644 --- a/src/main/java/it/smartcommunitylab/aac/config/AuditConfig.java +++ b/src/main/java/it/smartcommunitylab/aac/config/AuditConfig.java @@ -16,14 +16,22 @@ package it.smartcommunitylab.aac.config; -import it.smartcommunitylab.aac.audit.AuthenticationEventListener; -import it.smartcommunitylab.aac.audit.AuthorizationEventListener; -import it.smartcommunitylab.aac.audit.ExtendedAuthenticationEventPublisher; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.KeyUse; +import it.smartcommunitylab.aac.audit.listeners.AuthorizationEventListener; +import it.smartcommunitylab.aac.audit.listeners.ClientAuthenticationEventListener; +import it.smartcommunitylab.aac.audit.listeners.UserAuthenticationEventListener; import it.smartcommunitylab.aac.audit.store.AutoJdbcAuditEventStore; +import it.smartcommunitylab.aac.audit.store.SignedAuditDataReader; +import it.smartcommunitylab.aac.audit.store.SignedAuditDataWriter; +import it.smartcommunitylab.aac.events.ExtendedAuthenticationEventPublisher; import it.smartcommunitylab.aac.identity.service.IdentityProviderService; +import it.smartcommunitylab.aac.jose.JWKSetKeyStore; import it.smartcommunitylab.aac.oauth.event.OAuth2EventPublisher; import javax.sql.DataSource; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -31,6 +39,7 @@ import org.springframework.context.event.SimpleApplicationEventMulticaster; import org.springframework.core.annotation.Order; import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.util.StringUtils; /* * Audit configuration @@ -44,6 +53,18 @@ public class AuditConfig { @Autowired private DataSource dataSource; + @Value("${audit.issuer}") + private String issuer; + + @Value("${audit.kid.sig}") + private String sigKid; + + @Value("${audit.kid.enc}") + private String encKid; + + @Autowired + private JWKSetKeyStore jwtKeyStore; + @Bean(name = "applicationEventMulticaster") public ApplicationEventMulticaster simpleApplicationEventMulticaster() { SimpleApplicationEventMulticaster eventMulticaster = new SimpleApplicationEventMulticaster(); @@ -77,17 +98,44 @@ public OAuth2EventPublisher oauth2EventPublisher(ApplicationEventPublisher appli @Bean public AutoJdbcAuditEventStore auditEventRepository() { - return new AutoJdbcAuditEventStore(dataSource); + AutoJdbcAuditEventStore store = new AutoJdbcAuditEventStore(dataSource); + + if (StringUtils.hasText(sigKid)) { + //build signed converters + JWK jwk = jwtKeyStore + .getKeys() + .stream() + .filter(j -> + j.getKeyID().equals(sigKid) && (j.getKeyUse() == null || j.getKeyUse().equals(KeyUse.SIGNATURE)) + ) + .findFirst() + .orElse(null); + + if (jwk != null) { + SignedAuditDataWriter writer = new SignedAuditDataWriter(issuer, jwk); + SignedAuditDataReader reader = new SignedAuditDataReader(issuer, jwtKeyStore.getJwkSet()); + + store.setWriter(writer); + store.setReader(reader); + } + } + + return store; } @Bean - public AuthenticationEventListener authenticationEventListener(IdentityProviderService providerService) { - AuthenticationEventListener listener = new AuthenticationEventListener(); + public UserAuthenticationEventListener userAuthenticationEventListener(IdentityProviderService providerService) { + UserAuthenticationEventListener listener = new UserAuthenticationEventListener(); listener.setProviderService(providerService); return listener; } + @Bean + public ClientAuthenticationEventListener clientAuthenticationEventListener() { + return new ClientAuthenticationEventListener(); + } + @Bean public AuthorizationEventListener authorizationEventListener() { return new AuthorizationEventListener(); diff --git a/src/main/java/it/smartcommunitylab/aac/config/ConsoleSecurityConfig.java b/src/main/java/it/smartcommunitylab/aac/config/ConsoleSecurityConfig.java index e0a8b08ed..cd26f69ff 100644 --- a/src/main/java/it/smartcommunitylab/aac/config/ConsoleSecurityConfig.java +++ b/src/main/java/it/smartcommunitylab/aac/config/ConsoleSecurityConfig.java @@ -22,8 +22,10 @@ import java.util.List; import java.util.stream.Collectors; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.security.servlet.PathRequest; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.http.SessionCreationPolicy; @@ -82,6 +84,17 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http.build(); } + @Order(25) + @Bean("h2ConsoleSecurityFilterChain") + SecurityFilterChain h2ConsoleSecurityFilterChain(HttpSecurity http) throws Exception { + http + .requestMatcher(PathRequest.toH2Console()) + .authorizeHttpRequests(authorizeRequests -> authorizeRequests.anyRequest().permitAll()) + .csrf(csrf -> csrf.disable()) + .headers(headers -> headers.frameOptions(frame -> frame.sameOrigin())); + return http.build(); + } + public RequestMatcher getRequestMatcher() { // skip index.html to let auth entry point enforce login List matchers = Arrays diff --git a/src/main/java/it/smartcommunitylab/aac/console/DevAuditController.java b/src/main/java/it/smartcommunitylab/aac/console/DevAuditController.java index 4cf6676a0..084aeaf29 100644 --- a/src/main/java/it/smartcommunitylab/aac/console/DevAuditController.java +++ b/src/main/java/it/smartcommunitylab/aac/console/DevAuditController.java @@ -17,7 +17,7 @@ package it.smartcommunitylab.aac.console; import io.swagger.v3.oas.annotations.Hidden; -import it.smartcommunitylab.aac.audit.BaseAuditController; +import it.smartcommunitylab.aac.audit.controller.BaseAuditController; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; diff --git a/src/main/java/it/smartcommunitylab/aac/console/DevController.java b/src/main/java/it/smartcommunitylab/aac/console/DevController.java index 6607c73f0..8b09bedc9 100644 --- a/src/main/java/it/smartcommunitylab/aac/console/DevController.java +++ b/src/main/java/it/smartcommunitylab/aac/console/DevController.java @@ -21,7 +21,7 @@ import it.smartcommunitylab.aac.Config; import it.smartcommunitylab.aac.SystemKeys; import it.smartcommunitylab.aac.audit.AuditManager; -import it.smartcommunitylab.aac.audit.RealmAuditEvent; +import it.smartcommunitylab.aac.audit.model.RealmAuditEvent; import it.smartcommunitylab.aac.clients.ClientManager; import it.smartcommunitylab.aac.common.NoSuchRealmException; import it.smartcommunitylab.aac.common.NoSuchSubjectException; @@ -66,6 +66,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.actuate.audit.AuditEvent; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; @@ -243,7 +244,7 @@ public ResponseEntity getRealmStats( bean.setEvents(auditManager.countRealmEvents(realm, null, after, null)); bean.setLoginCount(auditManager.countRealmEvents(realm, "USER_AUTHENTICATION_SUCCESS", after, null)); - List loginEvents = auditManager + List loginEvents = auditManager .findRealmEvents(realm, "USER_AUTHENTICATION_SUCCESS", after, null) .stream() .limit(5) @@ -252,14 +253,14 @@ public ResponseEntity getRealmStats( Map d = new HashMap<>(e.getData()); d.remove("details"); - return new RealmAuditEvent(e.getRealm(), e.getTimestamp(), e.getPrincipal(), e.getType(), d); + return new AuditEvent(e.getTimestamp(), e.getPrincipal(), e.getType(), d); }) .collect(Collectors.toList()); bean.setLoginEvents(loginEvents); bean.setRegistrationCount(auditManager.countRealmEvents(realm, "USER_REGISTRATION", after, null)); - List registrationEvents = auditManager + List registrationEvents = auditManager .findRealmEvents(realm, "USER_REGISTRATION", after, null) .stream() .limit(5) @@ -268,7 +269,7 @@ public ResponseEntity getRealmStats( Map d = new HashMap<>(e.getData()); d.remove("details"); - return new RealmAuditEvent(e.getRealm(), e.getTimestamp(), e.getPrincipal(), e.getType(), d); + return new AuditEvent(e.getTimestamp(), e.getPrincipal(), e.getType(), d); }) .collect(Collectors.toList()); bean.setRegistrationEvents(registrationEvents); diff --git a/src/main/java/it/smartcommunitylab/aac/core/auth/ExtendedAuthenticationProvider.java b/src/main/java/it/smartcommunitylab/aac/core/auth/ExtendedAuthenticationProvider.java index 601d6a1fd..ca8fa3c09 100644 --- a/src/main/java/it/smartcommunitylab/aac/core/auth/ExtendedAuthenticationProvider.java +++ b/src/main/java/it/smartcommunitylab/aac/core/auth/ExtendedAuthenticationProvider.java @@ -22,6 +22,8 @@ import it.smartcommunitylab.aac.common.LoginException; import it.smartcommunitylab.aac.identity.model.UserAuthenticatedPrincipal; import java.time.Instant; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.core.Authentication; @@ -29,12 +31,19 @@ public abstract class ExtendedAuthenticationProvider

extends AbstractProvider

- implements AuthenticationProvider { + implements AuthenticationProvider, ApplicationEventPublisherAware { + + protected ApplicationEventPublisher eventPublisher; protected ExtendedAuthenticationProvider(String authority, String provider, String realm) { super(authority, provider, realm); } + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + // @Override // public final String getType() { // return SystemKeys.RESOURCE_AUTHENTICATION; diff --git a/src/main/java/it/smartcommunitylab/aac/dto/RealmStats.java b/src/main/java/it/smartcommunitylab/aac/dto/RealmStats.java index ba18dccaa..4ced9883d 100644 --- a/src/main/java/it/smartcommunitylab/aac/dto/RealmStats.java +++ b/src/main/java/it/smartcommunitylab/aac/dto/RealmStats.java @@ -16,9 +16,9 @@ package it.smartcommunitylab.aac.dto; -import it.smartcommunitylab.aac.audit.RealmAuditEvent; import it.smartcommunitylab.aac.model.Realm; import java.util.List; +import org.springframework.boot.actuate.audit.AuditEvent; /** * @author raman @@ -35,10 +35,10 @@ public class RealmStats { private Long events; private Long loginCount; - private List loginEvents; + private List loginEvents; private Long registrationCount; - private List registrationEvents; + private List registrationEvents; public Realm getRealm() { return realm; @@ -104,11 +104,11 @@ public void setLoginCount(Long loginCount) { this.loginCount = loginCount; } - public List getLoginEvents() { + public List getLoginEvents() { return loginEvents; } - public void setLoginEvents(List loginEvents) { + public void setLoginEvents(List loginEvents) { this.loginEvents = loginEvents; } @@ -120,11 +120,11 @@ public void setRegistrationCount(Long registrationCount) { this.registrationCount = registrationCount; } - public List getRegistrationEvents() { + public List getRegistrationEvents() { return registrationEvents; } - public void setRegistrationEvents(List registrationEvents) { + public void setRegistrationEvents(List registrationEvents) { this.registrationEvents = registrationEvents; } } diff --git a/src/main/java/it/smartcommunitylab/aac/audit/ExtendedAuthenticationEventPublisher.java b/src/main/java/it/smartcommunitylab/aac/events/ExtendedAuthenticationEventPublisher.java similarity index 99% rename from src/main/java/it/smartcommunitylab/aac/audit/ExtendedAuthenticationEventPublisher.java rename to src/main/java/it/smartcommunitylab/aac/events/ExtendedAuthenticationEventPublisher.java index 0523130ca..6e2aea77f 100644 --- a/src/main/java/it/smartcommunitylab/aac/audit/ExtendedAuthenticationEventPublisher.java +++ b/src/main/java/it/smartcommunitylab/aac/events/ExtendedAuthenticationEventPublisher.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package it.smartcommunitylab.aac.audit; +package it.smartcommunitylab.aac.events; import it.smartcommunitylab.aac.SystemKeys; import it.smartcommunitylab.aac.core.auth.ExtendedAuthenticationToken; diff --git a/src/main/java/it/smartcommunitylab/aac/events/ProviderEmittedEvent.java b/src/main/java/it/smartcommunitylab/aac/events/ProviderEmittedEvent.java new file mode 100644 index 000000000..2f6869352 --- /dev/null +++ b/src/main/java/it/smartcommunitylab/aac/events/ProviderEmittedEvent.java @@ -0,0 +1,26 @@ +/** + * Copyright 2024 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package it.smartcommunitylab.aac.events; + +/* + * An event emitted by an active provider must carry details about the provider itself + */ +public interface ProviderEmittedEvent { + String getAuthority(); + String getProvider(); + String getRealm(); +} diff --git a/src/main/java/it/smartcommunitylab/aac/audit/UserAuthenticationFailureEvent.java b/src/main/java/it/smartcommunitylab/aac/events/UserAuthenticationFailureEvent.java similarity index 98% rename from src/main/java/it/smartcommunitylab/aac/audit/UserAuthenticationFailureEvent.java rename to src/main/java/it/smartcommunitylab/aac/events/UserAuthenticationFailureEvent.java index 0c1c64792..1232364b0 100644 --- a/src/main/java/it/smartcommunitylab/aac/audit/UserAuthenticationFailureEvent.java +++ b/src/main/java/it/smartcommunitylab/aac/events/UserAuthenticationFailureEvent.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package it.smartcommunitylab.aac.audit; +package it.smartcommunitylab.aac.events; import it.smartcommunitylab.aac.SystemKeys; import it.smartcommunitylab.aac.core.auth.WebAuthenticationDetails; diff --git a/src/main/java/it/smartcommunitylab/aac/audit/UserAuthenticationSuccessEvent.java b/src/main/java/it/smartcommunitylab/aac/events/UserAuthenticationSuccessEvent.java similarity index 82% rename from src/main/java/it/smartcommunitylab/aac/audit/UserAuthenticationSuccessEvent.java rename to src/main/java/it/smartcommunitylab/aac/events/UserAuthenticationSuccessEvent.java index ea14e8977..e96f6e482 100644 --- a/src/main/java/it/smartcommunitylab/aac/audit/UserAuthenticationSuccessEvent.java +++ b/src/main/java/it/smartcommunitylab/aac/events/UserAuthenticationSuccessEvent.java @@ -14,11 +14,12 @@ * limitations under the License. */ -package it.smartcommunitylab.aac.audit; +package it.smartcommunitylab.aac.events; import it.smartcommunitylab.aac.SystemKeys; import it.smartcommunitylab.aac.core.auth.UserAuthentication; import it.smartcommunitylab.aac.core.auth.WebAuthenticationDetails; +import it.smartcommunitylab.aac.model.Subject; import org.springframework.security.authentication.event.AuthenticationSuccessEvent; import org.springframework.util.Assert; @@ -32,9 +33,11 @@ public class UserAuthenticationSuccessEvent extends AuthenticationSuccessEvent { private final String authority; private final String provider; private final String realm; + private final Subject subject; public UserAuthenticationSuccessEvent(String authority, String provider, String realm, UserAuthentication auth) { super(auth); + Assert.notNull(auth, "user auth is required"); Assert.hasText(authority, "authority is required"); Assert.notNull(provider, "provider is required"); Assert.notNull(realm, "realm is required"); @@ -42,9 +45,10 @@ public UserAuthenticationSuccessEvent(String authority, String provider, String this.authority = authority; this.provider = provider; this.realm = realm; + this.subject = auth.getSubject(); } - public UserAuthentication getAuthenticationToken() { + public UserAuthentication getUserAuthentication() { return (UserAuthentication) super.getAuthentication(); } @@ -60,7 +64,11 @@ public String getRealm() { return realm; } + public Subject getSubject() { + return subject; + } + public WebAuthenticationDetails getDetails() { - return getAuthenticationToken().getWebAuthenticationDetails(); + return getUserAuthentication().getWebAuthenticationDetails(); } } diff --git a/src/main/java/it/smartcommunitylab/aac/identity/base/AbstractIdentityProvider.java b/src/main/java/it/smartcommunitylab/aac/identity/base/AbstractIdentityProvider.java index 433a34577..14b0c8fef 100644 --- a/src/main/java/it/smartcommunitylab/aac/identity/base/AbstractIdentityProvider.java +++ b/src/main/java/it/smartcommunitylab/aac/identity/base/AbstractIdentityProvider.java @@ -39,6 +39,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.Assert; @@ -51,10 +53,12 @@ public abstract class AbstractIdentityProvider< C extends AbstractIdentityProviderConfig > extends AbstractConfigurableResourceProvider - implements IdentityProvider, InitializingBean { + implements IdentityProvider, ApplicationEventPublisherAware, InitializingBean { private final Logger logger = LoggerFactory.getLogger(getClass()); + protected ApplicationEventPublisher eventPublisher; + protected AbstractIdentityProvider(String authority, String providerId, C config, String realm) { super(authority, providerId, realm, config); Assert.notNull(config, "provider config is mandatory"); @@ -91,6 +95,11 @@ public boolean isAuthoritative() { return true; } + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + /* * Provider-specific */ diff --git a/src/main/java/it/smartcommunitylab/aac/internal/auth/InternalUserAuthenticationFailureEvent.java b/src/main/java/it/smartcommunitylab/aac/internal/auth/InternalUserAuthenticationFailureEvent.java index cf217b975..40d8448b3 100644 --- a/src/main/java/it/smartcommunitylab/aac/internal/auth/InternalUserAuthenticationFailureEvent.java +++ b/src/main/java/it/smartcommunitylab/aac/internal/auth/InternalUserAuthenticationFailureEvent.java @@ -17,7 +17,7 @@ package it.smartcommunitylab.aac.internal.auth; import it.smartcommunitylab.aac.SystemKeys; -import it.smartcommunitylab.aac.audit.UserAuthenticationFailureEvent; +import it.smartcommunitylab.aac.events.UserAuthenticationFailureEvent; import java.io.Serializable; import java.util.Map; import org.springframework.security.core.Authentication; diff --git a/src/main/java/it/smartcommunitylab/aac/oidc/auth/OIDCUserAuthenticationFailureEvent.java b/src/main/java/it/smartcommunitylab/aac/oidc/auth/OIDCUserAuthenticationFailureEvent.java index 5d4f06eae..e3ae3ec84 100644 --- a/src/main/java/it/smartcommunitylab/aac/oidc/auth/OIDCUserAuthenticationFailureEvent.java +++ b/src/main/java/it/smartcommunitylab/aac/oidc/auth/OIDCUserAuthenticationFailureEvent.java @@ -17,7 +17,7 @@ package it.smartcommunitylab.aac.oidc.auth; import it.smartcommunitylab.aac.SystemKeys; -import it.smartcommunitylab.aac.audit.UserAuthenticationFailureEvent; +import it.smartcommunitylab.aac.events.UserAuthenticationFailureEvent; import java.io.Serializable; import java.util.Map; import org.springframework.security.core.Authentication; diff --git a/src/main/java/it/smartcommunitylab/aac/oidc/events/OAuth2AuthorizationRequestEvent.java b/src/main/java/it/smartcommunitylab/aac/oidc/events/OAuth2AuthorizationRequestEvent.java new file mode 100644 index 000000000..c682eaf1b --- /dev/null +++ b/src/main/java/it/smartcommunitylab/aac/oidc/events/OAuth2AuthorizationRequestEvent.java @@ -0,0 +1,42 @@ +/** + * Copyright 2024 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package it.smartcommunitylab.aac.oidc.events; + +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.util.Assert; + +public class OAuth2AuthorizationRequestEvent extends OAuth2MessageEvent { + + public OAuth2AuthorizationRequestEvent( + String authority, + String provider, + String realm, + OAuth2AuthorizationRequest request + ) { + super(authority, provider, realm, request); + Assert.notNull(request, "request can not be null"); + } + + public OAuth2AuthorizationRequest getAuthorizationRequest() { + return (OAuth2AuthorizationRequest) super.getSource(); + } + + @Override + public String getTx() { + return getAuthorizationRequest().getState(); + } +} diff --git a/src/main/java/it/smartcommunitylab/aac/oidc/events/OAuth2AuthorizationResponseEvent.java b/src/main/java/it/smartcommunitylab/aac/oidc/events/OAuth2AuthorizationResponseEvent.java new file mode 100644 index 000000000..94f3779fc --- /dev/null +++ b/src/main/java/it/smartcommunitylab/aac/oidc/events/OAuth2AuthorizationResponseEvent.java @@ -0,0 +1,42 @@ +/** + * Copyright 2024 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package it.smartcommunitylab.aac.oidc.events; + +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse; +import org.springframework.util.Assert; + +public class OAuth2AuthorizationResponseEvent extends OAuth2MessageEvent { + + public OAuth2AuthorizationResponseEvent( + String authority, + String provider, + String realm, + OAuth2AuthorizationResponse response + ) { + super(authority, provider, realm, response); + Assert.notNull(response, "response can not be null"); + } + + public OAuth2AuthorizationResponse getAuthorizationResponse() { + return (OAuth2AuthorizationResponse) super.getSource(); + } + + @Override + public String getTx() { + return getAuthorizationResponse().getState(); + } +} diff --git a/src/main/java/it/smartcommunitylab/aac/oidc/events/OAuth2MessageEvent.java b/src/main/java/it/smartcommunitylab/aac/oidc/events/OAuth2MessageEvent.java new file mode 100644 index 000000000..024343a84 --- /dev/null +++ b/src/main/java/it/smartcommunitylab/aac/oidc/events/OAuth2MessageEvent.java @@ -0,0 +1,68 @@ +/** + * Copyright 2024 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package it.smartcommunitylab.aac.oidc.events; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import it.smartcommunitylab.aac.events.ProviderEmittedEvent; +import org.springframework.context.ApplicationEvent; +import org.springframework.util.Assert; + +public abstract class OAuth2MessageEvent extends ApplicationEvent implements ProviderEmittedEvent { + + private final String authority; + private final String provider; + private final String realm; + + private String tx; + + protected OAuth2MessageEvent(String authority, String provider, String realm, Object source) { + super(source); + Assert.hasText(provider, "provider identifier can not be null or blank"); + this.authority = authority; + this.provider = provider; + this.realm = realm; + } + + public String getAuthority() { + return authority; + } + + public String getProvider() { + return provider; + } + + public String getRealm() { + return realm; + } + + public void setTx(String tx) { + this.tx = tx; + } + + @JsonInclude(Include.NON_NULL) + public String getTx() { + return tx; + } + + @JsonIgnore + @Override + public Object getSource() { + return super.getSource(); + } +} diff --git a/src/main/java/it/smartcommunitylab/aac/oidc/events/OAuth2TokenRequestEvent.java b/src/main/java/it/smartcommunitylab/aac/oidc/events/OAuth2TokenRequestEvent.java new file mode 100644 index 000000000..5a6cf8c72 --- /dev/null +++ b/src/main/java/it/smartcommunitylab/aac/oidc/events/OAuth2TokenRequestEvent.java @@ -0,0 +1,32 @@ +/** + * Copyright 2024 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package it.smartcommunitylab.aac.oidc.events; + +import org.springframework.http.RequestEntity; +import org.springframework.util.Assert; + +public class OAuth2TokenRequestEvent extends OAuth2MessageEvent { + + public OAuth2TokenRequestEvent(String authority, String provider, String realm, RequestEntity request) { + super(authority, provider, realm, request); + Assert.notNull(request, "request can not be null"); + } + + public RequestEntity getRequestEntity() { + return (RequestEntity) super.getSource(); + } +} diff --git a/src/main/java/it/smartcommunitylab/aac/oidc/events/OAuth2TokenResponseEvent.java b/src/main/java/it/smartcommunitylab/aac/oidc/events/OAuth2TokenResponseEvent.java new file mode 100644 index 000000000..02db08571 --- /dev/null +++ b/src/main/java/it/smartcommunitylab/aac/oidc/events/OAuth2TokenResponseEvent.java @@ -0,0 +1,37 @@ +/** + * Copyright 2024 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package it.smartcommunitylab.aac.oidc.events; + +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.util.Assert; + +public class OAuth2TokenResponseEvent extends OAuth2MessageEvent { + + public OAuth2TokenResponseEvent( + String authority, + String provider, + String realm, + OAuth2AccessTokenResponse response + ) { + super(authority, provider, realm, response); + Assert.notNull(response, "response can not be null"); + } + + public OAuth2AccessTokenResponse getTokenResponse() { + return (OAuth2AccessTokenResponse) super.getSource(); + } +} diff --git a/src/main/java/it/smartcommunitylab/aac/oidc/events/OAuth2UserRequestEvent.java b/src/main/java/it/smartcommunitylab/aac/oidc/events/OAuth2UserRequestEvent.java new file mode 100644 index 000000000..07bc90a11 --- /dev/null +++ b/src/main/java/it/smartcommunitylab/aac/oidc/events/OAuth2UserRequestEvent.java @@ -0,0 +1,32 @@ +/** + * Copyright 2024 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package it.smartcommunitylab.aac.oidc.events; + +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.util.Assert; + +public class OAuth2UserRequestEvent extends OAuth2MessageEvent { + + public OAuth2UserRequestEvent(String authority, String provider, String realm, OAuth2UserRequest request) { + super(authority, provider, realm, request); + Assert.notNull(request, "request can not be null"); + } + + public OAuth2UserRequest getUserRequest() { + return (OAuth2UserRequest) super.getSource(); + } +} diff --git a/src/main/java/it/smartcommunitylab/aac/oidc/events/OAuth2UserResponseEvent.java b/src/main/java/it/smartcommunitylab/aac/oidc/events/OAuth2UserResponseEvent.java new file mode 100644 index 000000000..71782c5cb --- /dev/null +++ b/src/main/java/it/smartcommunitylab/aac/oidc/events/OAuth2UserResponseEvent.java @@ -0,0 +1,32 @@ +/** + * Copyright 2024 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package it.smartcommunitylab.aac.oidc.events; + +import java.util.Map; +import org.springframework.util.Assert; + +public class OAuth2UserResponseEvent extends OAuth2MessageEvent { + + public OAuth2UserResponseEvent(String authority, String provider, String realm, Map response) { + super(authority, provider, realm, response); + Assert.notNull(response, "response can not be null"); + } + + public Map getUserResponse() { + return (Map) super.getSource(); + } +} diff --git a/src/main/java/it/smartcommunitylab/aac/openidfed/OpenIdFedIdentityAuthority.java b/src/main/java/it/smartcommunitylab/aac/openidfed/OpenIdFedIdentityAuthority.java index 143149a07..360604be9 100644 --- a/src/main/java/it/smartcommunitylab/aac/openidfed/OpenIdFedIdentityAuthority.java +++ b/src/main/java/it/smartcommunitylab/aac/openidfed/OpenIdFedIdentityAuthority.java @@ -31,12 +31,15 @@ import it.smartcommunitylab.aac.openidfed.provider.OpenIdFedIdentityProviderConfig; import it.smartcommunitylab.aac.openidfed.provider.OpenIdFedIdentityProviderConfigMap; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.stereotype.Service; import org.springframework.util.Assert; @Service public class OpenIdFedIdentityAuthority - extends AbstractIdentityProviderAuthority { + extends AbstractIdentityProviderAuthority + implements ApplicationEventPublisherAware { public static final String AUTHORITY_URL = "/auth/" + SystemKeys.AUTHORITY_OPENIDFED + "/"; @@ -49,6 +52,7 @@ public class OpenIdFedIdentityAuthority // execution service for custom attributes mapping private ScriptExecutionService executionService; private ResourceEntityService resourceService; + private ApplicationEventPublisher eventPublisher; @Autowired public OpenIdFedIdentityAuthority( @@ -92,6 +96,12 @@ public void setRealmAwareUriBuilder(RealmAwareUriBuilder realmAwareUriBuilder) { this.filterProvider.setRealmAwareUriBuilder(realmAwareUriBuilder); } + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + this.filterProvider.setApplicationEventPublisher(eventPublisher); + } + @Override public OpenIdFedFilterProvider getFilterProvider() { return this.filterProvider; @@ -111,6 +121,7 @@ public OpenIdFedIdentityProvider buildProvider(OpenIdFedIdentityProviderConfig c idp.setExecutionService(executionService); idp.setResourceService(resourceService); + idp.setApplicationEventPublisher(eventPublisher); return idp; } } diff --git a/src/main/java/it/smartcommunitylab/aac/openidfed/audit/OAuth2UserRequestMixins.java b/src/main/java/it/smartcommunitylab/aac/openidfed/audit/OAuth2UserRequestMixins.java new file mode 100644 index 000000000..6daa08ca3 --- /dev/null +++ b/src/main/java/it/smartcommunitylab/aac/openidfed/audit/OAuth2UserRequestMixins.java @@ -0,0 +1,22 @@ +/** + * Copyright 2024 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package it.smartcommunitylab.aac.openidfed.audit; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties({ "clientRegistration" }) +public interface OAuth2UserRequestMixins {} diff --git a/src/main/java/it/smartcommunitylab/aac/openidfed/audit/OpenIdFedAuditEventListener.java b/src/main/java/it/smartcommunitylab/aac/openidfed/audit/OpenIdFedAuditEventListener.java new file mode 100644 index 000000000..e41bf7b9f --- /dev/null +++ b/src/main/java/it/smartcommunitylab/aac/openidfed/audit/OpenIdFedAuditEventListener.java @@ -0,0 +1,138 @@ +/** + * Copyright 2024 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package it.smartcommunitylab.aac.openidfed.audit; + +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import it.smartcommunitylab.aac.SystemKeys; +import it.smartcommunitylab.aac.audit.store.AuditApplicationEventMixIns; +import it.smartcommunitylab.aac.core.provider.ProviderConfigRepository; +import it.smartcommunitylab.aac.oidc.events.OAuth2MessageEvent; +import it.smartcommunitylab.aac.openidfed.events.OpenIdFedMessageEvent; +import it.smartcommunitylab.aac.openidfed.provider.OpenIdFedIdentityProviderConfig; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.audit.listener.AuditApplicationEvent; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +@Component +public class OpenIdFedAuditEventListener + implements ApplicationListener, ApplicationEventPublisherAware { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private static final String OPENIDFED_MESSAGE = "OPENIDFED_MESSAGE"; + + private final ObjectMapper mapper = new ObjectMapper() + .registerModule(new JavaTimeModule()) + //include only non-null fields + .setSerializationInclusion(Include.NON_NULL) + //add mixin for including typeInfo in events + .addMixIn(ApplicationEvent.class, AuditApplicationEventMixIns.class); + private final TypeReference> typeRef = new TypeReference>() {}; + + private ApplicationEventPublisher publisher; + + private ProviderConfigRepository registrationRepository; + + @Autowired + public void setRegistrationRepository( + ProviderConfigRepository registrationRepository + ) { + this.registrationRepository = registrationRepository; + } + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher publisher) { + this.publisher = publisher; + } + + @Override + public void onApplicationEvent(OpenIdFedMessageEvent event) { + String authority = event.getAuthority(); + String provider = event.getProvider(); + String realm = event.getRealm(); + String trustAnchor = event.getTrustAnchor(); + String entityId = event.getEntityId(); + + logger.debug( + "receive openidfed message event for {}:{} entity {}", + authority, + provider, + String.valueOf(entityId) + ); + + if (registrationRepository == null || publisher == null) { + logger.debug("invalid configuration, skip event"); + return; + } + + OpenIdFedIdentityProviderConfig config = registrationRepository.findByProviderId(provider); + if (config == null) { + logger.debug("missing provider configuration, skip event"); + return; + } + + String level = config.getSettingsMap().getEvents(); + if (SystemKeys.EVENTS_LEVEL_NONE.equals(level)) { + logger.debug("provider configuration level none, skip event"); + return; + } + + Map data = new HashMap<>(); + data.put("authority", authority); + data.put("provider", provider); + data.put("realm", realm); + + if (StringUtils.hasText(entityId)) { + data.put("entityId", entityId); + } + if (StringUtils.hasText(trustAnchor)) { + data.put("trustAnchor", trustAnchor); + } + + if (SystemKeys.EVENTS_LEVEL_FULL.equals(level)) { + //serialize to avoid exposing object to audit + data.put("event", mapper.convertValue(event, typeRef)); + } + + AuditApplicationEvent auditEvent = new AuditApplicationEvent( + Instant.ofEpochMilli(event.getTimestamp()), + provider, + OPENIDFED_MESSAGE, + data + ); + + logger.debug("publish openid message event for audit"); + if (logger.isTraceEnabled()) { + logger.trace("audit event: {}", auditEvent); + } + + publisher.publishEvent(auditEvent); + } +} diff --git a/src/main/java/it/smartcommunitylab/aac/openidfed/audit/OpenIdFedOAuth2AuditEventListener.java b/src/main/java/it/smartcommunitylab/aac/openidfed/audit/OpenIdFedOAuth2AuditEventListener.java new file mode 100644 index 000000000..1486d3b69 --- /dev/null +++ b/src/main/java/it/smartcommunitylab/aac/openidfed/audit/OpenIdFedOAuth2AuditEventListener.java @@ -0,0 +1,130 @@ +/** + * Copyright 2024 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package it.smartcommunitylab.aac.openidfed.audit; + +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import it.smartcommunitylab.aac.SystemKeys; +import it.smartcommunitylab.aac.audit.store.AuditApplicationEventMixIns; +import it.smartcommunitylab.aac.core.provider.ProviderConfigRepository; +import it.smartcommunitylab.aac.oidc.events.OAuth2MessageEvent; +import it.smartcommunitylab.aac.openidfed.provider.OpenIdFedIdentityProviderConfig; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.audit.listener.AuditApplicationEvent; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.context.ApplicationListener; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +@Component +public class OpenIdFedOAuth2AuditEventListener + implements ApplicationListener, ApplicationEventPublisherAware { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private static final String OIDC_MESSAGE = "OIDC_MESSAGE"; + + private final ObjectMapper mapper = new ObjectMapper() + .registerModule(new JavaTimeModule()) + //include only non-null fields + .setSerializationInclusion(Include.NON_NULL) + //add mixin for including typeInfo in events + .addMixIn(ApplicationEvent.class, AuditApplicationEventMixIns.class) + .addMixIn(OAuth2UserRequest.class, OAuth2UserRequestMixins.class); + private final TypeReference> typeRef = new TypeReference>() {}; + + private ApplicationEventPublisher publisher; + + private ProviderConfigRepository registrationRepository; + + @Autowired + public void setRegistrationRepository( + ProviderConfigRepository registrationRepository + ) { + this.registrationRepository = registrationRepository; + } + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher publisher) { + this.publisher = publisher; + } + + @Override + public void onApplicationEvent(OAuth2MessageEvent event) { + String authority = event.getAuthority(); + String provider = event.getProvider(); + String realm = event.getRealm(); + String tx = event.getTx(); + + logger.debug("receive openidfed message event for {}:{} key {}", authority, provider, String.valueOf(tx)); + + if (registrationRepository == null || publisher == null) { + logger.debug("invalid configuration, skip event"); + return; + } + + OpenIdFedIdentityProviderConfig config = registrationRepository.findByProviderId(provider); + if (config == null) { + logger.debug("missing provider configuration, skip event"); + return; + } + + String level = config.getSettingsMap().getEvents(); + if (SystemKeys.EVENTS_LEVEL_NONE.equals(level)) { + logger.debug("provider configuration level none, skip event"); + return; + } + + Map data = new HashMap<>(); + data.put("authority", authority); + data.put("provider", provider); + data.put("realm", realm); + + if (StringUtils.hasText(tx)) { + data.put("tx", tx); + } + + if (SystemKeys.EVENTS_LEVEL_FULL.equals(level)) { + //serialize to avoid exposing object to audit + data.put("event", mapper.convertValue(event, typeRef)); + } + + AuditApplicationEvent auditEvent = new AuditApplicationEvent( + Instant.ofEpochMilli(event.getTimestamp()), + provider, + OIDC_MESSAGE, + data + ); + + logger.debug("publish openid message event for audit"); + if (logger.isTraceEnabled()) { + logger.trace("audit event: {}", auditEvent); + } + + publisher.publishEvent(auditEvent); + } +} diff --git a/src/main/java/it/smartcommunitylab/aac/openidfed/auth/OpenIdFedAuthorizationCodeRequestEntityConverter.java b/src/main/java/it/smartcommunitylab/aac/openidfed/auth/OpenIdFedAuthorizationCodeRequestEntityConverter.java new file mode 100644 index 000000000..b9e9c9526 --- /dev/null +++ b/src/main/java/it/smartcommunitylab/aac/openidfed/auth/OpenIdFedAuthorizationCodeRequestEntityConverter.java @@ -0,0 +1,76 @@ +/** + * Copyright 2024 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package it.smartcommunitylab.aac.openidfed.auth; + +import com.nimbusds.jose.jwk.JWK; +import it.smartcommunitylab.aac.oidc.events.OAuth2TokenRequestEvent; +import it.smartcommunitylab.aac.openidfed.provider.OpenIdFedIdentityProviderConfig; +import java.util.function.Function; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.http.RequestEntity; +import org.springframework.security.oauth2.client.endpoint.NimbusJwtClientAuthenticationParametersConverter; +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequestEntityConverter; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.util.Assert; + +public class OpenIdFedAuthorizationCodeRequestEntityConverter + extends OAuth2AuthorizationCodeGrantRequestEntityConverter + implements ApplicationEventPublisherAware { + + private final OpenIdFedIdentityProviderConfig config; + + private ApplicationEventPublisher eventPublisher; + + public OpenIdFedAuthorizationCodeRequestEntityConverter(OpenIdFedIdentityProviderConfig config) { + Assert.notNull(config, "provider config is required"); + this.config = config; + + // private key jwt resolver, as per + // https://tools.ietf.org/html/rfc7523#section-2.2 + // fetch key + JWK jwk = config.getClientSignatureJWK(); + + // build resolver only for this registration to retrieve client key + Function jwkResolver = clientRegistration -> jwk; + addParametersConverter(new NimbusJwtClientAuthenticationParametersConverter<>(jwkResolver)); + } + + @Override + public RequestEntity convert(OAuth2AuthorizationCodeGrantRequest authorizationGrantRequest) { + RequestEntity request = super.convert(authorizationGrantRequest); + if (eventPublisher != null) { + OAuth2TokenRequestEvent event = new OAuth2TokenRequestEvent( + config.getAuthority(), + config.getProvider(), + config.getRealm(), + request + ); + event.setTx(authorizationGrantRequest.getAuthorizationExchange().getAuthorizationRequest().getState()); + + eventPublisher.publishEvent(event); + } + + return request; + } + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } +} diff --git a/src/main/java/it/smartcommunitylab/aac/openidfed/auth/OpenIdFedAuthorizationCodeTokenResponseClient.java b/src/main/java/it/smartcommunitylab/aac/openidfed/auth/OpenIdFedAuthorizationCodeTokenResponseClient.java new file mode 100644 index 000000000..34e10553b --- /dev/null +++ b/src/main/java/it/smartcommunitylab/aac/openidfed/auth/OpenIdFedAuthorizationCodeTokenResponseClient.java @@ -0,0 +1,74 @@ +/** + * Copyright 2024 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package it.smartcommunitylab.aac.openidfed.auth; + +import it.smartcommunitylab.aac.oidc.events.OAuth2TokenResponseEvent; +import it.smartcommunitylab.aac.openidfed.provider.OpenIdFedIdentityProviderConfig; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationCodeTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.util.Assert; + +public class OpenIdFedAuthorizationCodeTokenResponseClient + implements OAuth2AccessTokenResponseClient, ApplicationEventPublisherAware { + + private final OpenIdFedIdentityProviderConfig config; + + private final OpenIdFedAuthorizationCodeRequestEntityConverter requestEntityConverter; + private final DefaultAuthorizationCodeTokenResponseClient accessTokenResponseClient; + + private ApplicationEventPublisher eventPublisher; + + public OpenIdFedAuthorizationCodeTokenResponseClient(OpenIdFedIdentityProviderConfig config) { + Assert.notNull(config, "provider config is required"); + this.config = config; + + requestEntityConverter = new OpenIdFedAuthorizationCodeRequestEntityConverter(config); + requestEntityConverter.setApplicationEventPublisher(eventPublisher); + + accessTokenResponseClient = new DefaultAuthorizationCodeTokenResponseClient(); + accessTokenResponseClient.setRequestEntityConverter(requestEntityConverter); + } + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + this.requestEntityConverter.setApplicationEventPublisher(eventPublisher); + } + + @Override + public OAuth2AccessTokenResponse getTokenResponse(OAuth2AuthorizationCodeGrantRequest authorizationGrantRequest) { + OAuth2AccessTokenResponse response = accessTokenResponseClient.getTokenResponse(authorizationGrantRequest); + + if (eventPublisher != null) { + OAuth2TokenResponseEvent event = new OAuth2TokenResponseEvent( + config.getAuthority(), + config.getProvider(), + config.getRealm(), + response + ); + event.setTx(authorizationGrantRequest.getAuthorizationExchange().getAuthorizationRequest().getState()); + + eventPublisher.publishEvent(event); + } + + return response; + } +} diff --git a/src/main/java/it/smartcommunitylab/aac/openidfed/auth/OpenIdFedLoginAuthenticationFilter.java b/src/main/java/it/smartcommunitylab/aac/openidfed/auth/OpenIdFedLoginAuthenticationFilter.java index a6cc3e97a..a31888f89 100644 --- a/src/main/java/it/smartcommunitylab/aac/openidfed/auth/OpenIdFedLoginAuthenticationFilter.java +++ b/src/main/java/it/smartcommunitylab/aac/openidfed/auth/OpenIdFedLoginAuthenticationFilter.java @@ -22,6 +22,7 @@ import it.smartcommunitylab.aac.core.auth.UserAuthentication; import it.smartcommunitylab.aac.core.auth.WebAuthenticationDetails; import it.smartcommunitylab.aac.core.provider.ProviderConfigRepository; +import it.smartcommunitylab.aac.oidc.events.OAuth2AuthorizationResponseEvent; import it.smartcommunitylab.aac.openidfed.OpenIdFedIdentityAuthority; import it.smartcommunitylab.aac.openidfed.provider.OpenIdFedIdentityProviderConfig; import java.io.IOException; @@ -31,6 +32,8 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken; @@ -60,6 +63,8 @@ */ public class OpenIdFedLoginAuthenticationFilter extends AbstractAuthenticationProcessingFilter { + private final Logger logger = LoggerFactory.getLogger(getClass()); + public static final String DEFAULT_FILTER_URI = OpenIdFedIdentityAuthority.AUTHORITY_URL + "login/{providerId}"; public static final String DEFAULT_EXTERNAL_REQ_STATE = "externalRequestDefaultStateString"; @@ -226,6 +231,15 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ // convert request to response OAuth2AuthorizationResponse authorizationResponse = convert(params, redirectUri); + //publish event + if (eventPublisher != null) { + logger.debug("publish event for authorization response {}", authorizationRequest.getState()); + + eventPublisher.publishEvent( + new OAuth2AuthorizationResponseEvent(authority, providerId, realm, authorizationResponse) + ); + } + //check if we received an error if (authorizationResponse.getError() != null) { throw new OAuth2AuthenticationException(authorizationResponse.getError()); diff --git a/src/main/java/it/smartcommunitylab/aac/openidfed/auth/OpenIdFedOAuth2AuthorizationRequestResolver.java b/src/main/java/it/smartcommunitylab/aac/openidfed/auth/OpenIdFedOAuth2AuthorizationRequestResolver.java index 7f3579e56..acdafc1e2 100644 --- a/src/main/java/it/smartcommunitylab/aac/openidfed/auth/OpenIdFedOAuth2AuthorizationRequestResolver.java +++ b/src/main/java/it/smartcommunitylab/aac/openidfed/auth/OpenIdFedOAuth2AuthorizationRequestResolver.java @@ -76,7 +76,7 @@ public OpenIdFedOAuth2AuthorizationRequestResolver( this.registrationRepository = registrationRepository; this.authorizationRequestBaseUri = authorizationRequestBaseUri; - this.requestMatcher = new AntPathRequestMatcher(authorizationRequestBaseUri + "/{providerId}/{registrationId}"); + this.requestMatcher = new AntPathRequestMatcher(authorizationRequestBaseUri + "/{registrationId}"); } @Override @@ -114,7 +114,7 @@ private OAuth2AuthorizationRequest resolve( //build a scoped default resolver DefaultOAuth2AuthorizationRequestResolver defaultResolver = new DefaultOAuth2AuthorizationRequestResolver( config.getClientRegistrationRepository(), - authorizationRequestBaseUri + "/" + providerId + authorizationRequestBaseUri ); //add PKCE diff --git a/src/main/java/it/smartcommunitylab/aac/openidfed/auth/OpenIdFedRedirectAuthenticationFilter.java b/src/main/java/it/smartcommunitylab/aac/openidfed/auth/OpenIdFedRedirectAuthenticationFilter.java index 52e4265e3..f6ecf24e6 100644 --- a/src/main/java/it/smartcommunitylab/aac/openidfed/auth/OpenIdFedRedirectAuthenticationFilter.java +++ b/src/main/java/it/smartcommunitylab/aac/openidfed/auth/OpenIdFedRedirectAuthenticationFilter.java @@ -18,11 +18,30 @@ import it.smartcommunitylab.aac.SystemKeys; import it.smartcommunitylab.aac.core.provider.ProviderConfigRepository; +import it.smartcommunitylab.aac.oidc.events.OAuth2AuthorizationRequestEvent; import it.smartcommunitylab.aac.openidfed.OpenIdFedIdentityAuthority; import it.smartcommunitylab.aac.openidfed.provider.OpenIdFedIdentityProviderConfig; +import java.io.IOException; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.http.HttpStatus; import org.springframework.lang.Nullable; -import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter; +import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; +import org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizationRequestRepository; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.web.DefaultRedirectStrategy; +import org.springframework.security.web.RedirectStrategy; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; +import org.springframework.web.filter.OncePerRequestFilter; /* * Build authorization request for OpenIdFed @@ -30,11 +49,25 @@ * Note: use a custom filter to make sure oncePerRequest uses our name to check execution */ -public class OpenIdFedRedirectAuthenticationFilter extends OAuth2AuthorizationRequestRedirectFilter { +public class OpenIdFedRedirectAuthenticationFilter + extends OncePerRequestFilter + implements ApplicationEventPublisherAware { - public static final String DEFAULT_FILTER_URI = OpenIdFedIdentityAuthority.AUTHORITY_URL + "authorize"; + private final Logger logger = LoggerFactory.getLogger(getClass()); + + public static final String DEFAULT_FILTER_URI = OpenIdFedIdentityAuthority.AUTHORITY_URL + "authorize/{providerId}"; + + private final RequestMatcher requestMatcher; + private final RedirectStrategy authorizationRedirectStrategy = new DefaultRedirectStrategy(); + private OAuth2AuthorizationRequestResolver authorizationRequestResolver; + + private AuthorizationRequestRepository authorizationRequestRepository = + new HttpSessionOAuth2AuthorizationRequestRepository(); private final String authority; + private final ProviderConfigRepository registrationRepository; + + private ApplicationEventPublisher eventPublisher; public OpenIdFedRedirectAuthenticationFilter( ProviderConfigRepository registrationRepository @@ -45,18 +78,104 @@ public OpenIdFedRedirectAuthenticationFilter( public OpenIdFedRedirectAuthenticationFilter( String authority, ProviderConfigRepository registrationRepository, - String filterProcessesUrl + String filterProcessingUrl ) { - // set openid fed request resolver - super(new OpenIdFedOAuth2AuthorizationRequestResolver(registrationRepository, filterProcessesUrl)); Assert.hasText(authority, "authority can not be null or empty"); Assert.notNull(registrationRepository, "provider registration repository cannot be null"); this.authority = authority; + this.registrationRepository = registrationRepository; + this.authorizationRequestResolver = + new OpenIdFedOAuth2AuthorizationRequestResolver(registrationRepository, filterProcessingUrl); + this.requestMatcher = new AntPathRequestMatcher(filterProcessingUrl + "/**"); } @Nullable protected String getFilterName() { return getClass().getName() + "." + authority; } + + public void setAuthorizationRequestRepository( + AuthorizationRequestRepository authorizationRequestRepository + ) { + this.authorizationRequestRepository = authorizationRequestRepository; + } + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + if (!requestMatcher.matches(request)) { + filterChain.doFilter(request, response); + return; + } + + try { + logger.debug("resolving authorization request from http request"); + OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestResolver.resolve(request); + if (authorizationRequest == null) { + logger.debug("error resolving authorization request from http request"); + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } + + //resolve provider + String providerId = resolveProviderId(request); + OpenIdFedIdentityProviderConfig config = registrationRepository.findByProviderId(providerId); + if (config == null) { + logger.error("error retrieving provider for registration {}", String.valueOf(providerId)); + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + return; + } + + //save + logger.debug( + "persisting authorization request with context under key: {}", + authorizationRequest.getState() + ); + authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response); + + //publish event + if (eventPublisher != null) { + logger.debug("publish event for authorization request {}", authorizationRequest.getState()); + + eventPublisher.publishEvent( + new OAuth2AuthorizationRequestEvent( + authority, + config.getProvider(), + config.getRealm(), + authorizationRequest + ) + ); + } + + //send redirect to client + logger.debug("send redirect to client for authorization request {}", authorizationRequest.getState()); + if (logger.isTraceEnabled()) { + logger.trace("redirectUri: {}", authorizationRequest.getAuthorizationRequestUri()); + } + + this.authorizationRedirectStrategy.sendRedirect( + request, + response, + authorizationRequest.getAuthorizationRequestUri() + ); + } catch (Exception ex) { + response.sendError( + HttpStatus.INTERNAL_SERVER_ERROR.value(), + HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase() + ); + } + } + + private String resolveProviderId(HttpServletRequest request) { + if (this.requestMatcher.matches(request)) { + return this.requestMatcher.matcher(request).getVariables().get("providerId"); + } + return null; + } } diff --git a/src/main/java/it/smartcommunitylab/aac/openidfed/auth/OpenIdFedUserRequestEntityConverter.java b/src/main/java/it/smartcommunitylab/aac/openidfed/auth/OpenIdFedUserRequestEntityConverter.java new file mode 100644 index 000000000..a389409fc --- /dev/null +++ b/src/main/java/it/smartcommunitylab/aac/openidfed/auth/OpenIdFedUserRequestEntityConverter.java @@ -0,0 +1,61 @@ +/** + * Copyright 2024 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package it.smartcommunitylab.aac.openidfed.auth; + +import it.smartcommunitylab.aac.oidc.events.OAuth2UserRequestEvent; +import it.smartcommunitylab.aac.openidfed.provider.OpenIdFedIdentityProviderConfig; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.http.RequestEntity; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequestEntityConverter; +import org.springframework.util.Assert; + +public class OpenIdFedUserRequestEntityConverter + extends OAuth2UserRequestEntityConverter + implements ApplicationEventPublisherAware { + + private final OpenIdFedIdentityProviderConfig config; + + private ApplicationEventPublisher eventPublisher; + + public OpenIdFedUserRequestEntityConverter(OpenIdFedIdentityProviderConfig config) { + Assert.notNull(config, "provider config is required"); + this.config = config; + } + + @Override + public RequestEntity convert(OAuth2UserRequest userRequest) { + if (eventPublisher != null) { + OAuth2UserRequestEvent event = new OAuth2UserRequestEvent( + config.getAuthority(), + config.getProvider(), + config.getRealm(), + userRequest + ); + + eventPublisher.publishEvent(event); + } + + return super.convert(userRequest); + } + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } +} diff --git a/src/main/java/it/smartcommunitylab/aac/openidfed/events/OpenIdFedMessageEvent.java b/src/main/java/it/smartcommunitylab/aac/openidfed/events/OpenIdFedMessageEvent.java new file mode 100644 index 000000000..e82d23b24 --- /dev/null +++ b/src/main/java/it/smartcommunitylab/aac/openidfed/events/OpenIdFedMessageEvent.java @@ -0,0 +1,78 @@ +/** + * Copyright 2024 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package it.smartcommunitylab.aac.openidfed.events; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import it.smartcommunitylab.aac.events.ProviderEmittedEvent; +import org.springframework.context.ApplicationEvent; +import org.springframework.util.Assert; + +public abstract class OpenIdFedMessageEvent extends ApplicationEvent implements ProviderEmittedEvent { + + private final String authority; + private final String provider; + private final String realm; + + private String trustAnchor; + private String entityId; + + protected OpenIdFedMessageEvent(String authority, String provider, String realm, Object source) { + super(source); + Assert.hasText(provider, "provider identifier can not be null or blank"); + this.authority = authority; + this.provider = provider; + this.realm = realm; + } + + public String getAuthority() { + return authority; + } + + public String getProvider() { + return provider; + } + + public String getRealm() { + return realm; + } + + @JsonInclude(Include.NON_NULL) + public String getTrustAnchor() { + return trustAnchor; + } + + public void setTrustAnchor(String trustAnchor) { + this.trustAnchor = trustAnchor; + } + + @JsonInclude(Include.NON_NULL) + public String getEntityId() { + return entityId; + } + + public void setEntityId(String entityId) { + this.entityId = entityId; + } + + @JsonIgnore + @Override + public Object getSource() { + return super.getSource(); + } +} diff --git a/src/main/java/it/smartcommunitylab/aac/openidfed/events/OpenIdTrustChainResolvedEvent.java b/src/main/java/it/smartcommunitylab/aac/openidfed/events/OpenIdTrustChainResolvedEvent.java new file mode 100644 index 000000000..32e8170d3 --- /dev/null +++ b/src/main/java/it/smartcommunitylab/aac/openidfed/events/OpenIdTrustChainResolvedEvent.java @@ -0,0 +1,32 @@ +/** + * Copyright 2024 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package it.smartcommunitylab.aac.openidfed.events; + +import com.nimbusds.openid.connect.sdk.federation.trust.TrustChain; +import org.springframework.util.Assert; + +public class OpenIdTrustChainResolvedEvent extends OpenIdFedMessageEvent { + + public OpenIdTrustChainResolvedEvent(String authority, String provider, String realm, TrustChain trustChain) { + super(authority, provider, realm, trustChain); + Assert.notNull(trustChain, "trustChain can not be null"); + } + + public TrustChain getTrustChain() { + return (TrustChain) super.getSource(); + } +} diff --git a/src/main/java/it/smartcommunitylab/aac/openidfed/provider/OpenIdFedAuthenticationProvider.java b/src/main/java/it/smartcommunitylab/aac/openidfed/provider/OpenIdFedAuthenticationProvider.java index 1cd7bffcc..784607081 100644 --- a/src/main/java/it/smartcommunitylab/aac/openidfed/provider/OpenIdFedAuthenticationProvider.java +++ b/src/main/java/it/smartcommunitylab/aac/openidfed/provider/OpenIdFedAuthenticationProvider.java @@ -16,9 +16,6 @@ package it.smartcommunitylab.aac.openidfed.provider; -import com.nimbusds.jose.jwk.JWK; -import com.nimbusds.jose.jwk.JWKSet; -import com.nimbusds.jose.jwk.source.JWKSource; import it.smartcommunitylab.aac.Config; import it.smartcommunitylab.aac.SystemKeys; import it.smartcommunitylab.aac.accounts.persistence.UserAccountService; @@ -30,38 +27,27 @@ import it.smartcommunitylab.aac.common.SystemException; import it.smartcommunitylab.aac.core.auth.ExtendedAuthenticationProvider; import it.smartcommunitylab.aac.identity.provider.IdentityProvider; -import it.smartcommunitylab.aac.jwt.JoseRestOperations; import it.smartcommunitylab.aac.oidc.OIDCKeys; import it.smartcommunitylab.aac.oidc.auth.OIDCAuthenticationException; import it.smartcommunitylab.aac.oidc.auth.OIDCAuthenticationToken; import it.smartcommunitylab.aac.oidc.auth.OIDCIdTokenDecoderFactory; import it.smartcommunitylab.aac.oidc.model.OIDCUserAccount; import it.smartcommunitylab.aac.oidc.model.OIDCUserAuthenticatedPrincipal; +import it.smartcommunitylab.aac.openidfed.auth.OpenIdFedAuthorizationCodeTokenResponseClient; +import it.smartcommunitylab.aac.openidfed.service.OpenIdFedOidcUserService; import java.io.Serializable; -import java.text.ParseException; import java.util.Collections; import java.util.Map; -import java.util.function.Function; import java.util.stream.Collectors; -import net.minidev.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; -import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationProvider; import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken; -import org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationCodeTokenResponseClient; -import org.springframework.security.oauth2.client.endpoint.NimbusJwtClientAuthenticationParametersConverter; -import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; -import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; -import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequestEntityConverter; import org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeAuthenticationProvider; -import org.springframework.security.oauth2.client.oidc.authentication.OidcIdTokenDecoderFactory; -import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; -import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; @@ -83,23 +69,9 @@ public class OpenIdFedAuthenticationProvider protected ScriptExecutionService executionService; protected final OpenIdAttributesMapper openidMapper; - private final OAuth2AccessTokenResponseClient accessTokenResponseClient; - - // private final LoadingCache> oauth2Services = CacheBuilder - // .newBuilder() - // .expireAfterWrite(5, TimeUnit.MINUTES) - // .build( - // new CacheLoader>() { - // @Override - // public OAuth2UserService load(String key) throws Exception { - // return federationResolver - // .listFederationEntities(trustAnchor, key, EntityType.OPENID_PROVIDER) - // .stream() - // .map(e -> e.getValue()) - // .collect(Collectors.toList()); - // } - // } - // ); + private final OidcAuthorizationCodeAuthenticationProvider oidcProvider; + private OpenIdFedOidcUserService userService; + private OpenIdFedAuthorizationCodeTokenResponseClient accessTokenResponseClient; public OpenIdFedAuthenticationProvider( String providerId, @@ -130,36 +102,31 @@ public OpenIdFedAuthenticationProvider( // attribute mapper to extract email this.openidMapper = new OpenIdAttributesMapper(); - // build appropriate client auth request converter - OAuth2AuthorizationCodeGrantRequestEntityConverter requestEntityConverter = - new OAuth2AuthorizationCodeGrantRequestEntityConverter(); - - // private key jwt resolver, as per - // https://tools.ietf.org/html/rfc7523#section-2.2 - // fetch key - JWK jwk = config.getClientSignatureJWK(); - // build resolver only for this registration to retrieve client key - Function jwkResolver = clientRegistration -> jwk; - requestEntityConverter.addParametersConverter( - new NimbusJwtClientAuthenticationParametersConverter<>(jwkResolver) - ); + userService = new OpenIdFedOidcUserService(config); + + accessTokenResponseClient = new OpenIdFedAuthorizationCodeTokenResponseClient(config); - // we support only authCode login - DefaultAuthorizationCodeTokenResponseClient responseClient = new DefaultAuthorizationCodeTokenResponseClient(); - responseClient.setRequestEntityConverter(requestEntityConverter); + oidcProvider = new OidcAuthorizationCodeAuthenticationProvider(accessTokenResponseClient, userService); - this.accessTokenResponseClient = responseClient; - //build rest template with support for jwt/jose - //TODO remove and build a custom oauth2userservice because we can not know keys beforehand - // JoseRestOperations restOperations = new JoseRestOperations(realm); + // replace jwtDecoderFactory to support providers with jwks in place of jwksUri + oidcProvider.setJwtDecoderFactory(new OIDCIdTokenDecoderFactory()); + // use a custom authorities mapper to cleanup authorities spring injects + // default impl translates the whole oauth response as an authority.. + oidcProvider.setAuthoritiesMapper(nullAuthoritiesMapper); + } + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) { + super.setApplicationEventPublisher(eventPublisher); + userService.setApplicationEventPublisher(eventPublisher); + accessTokenResponseClient.setApplicationEventPublisher(eventPublisher); } @Override public Authentication doAuthenticate(Authentication authentication) throws AuthenticationException { OAuth2LoginAuthenticationToken loginAuthenticationToken = (OAuth2LoginAuthenticationToken) authentication; - // TODO extract codeResponse + tokenResponse for audit String authorizationRequestUri = loginAuthenticationToken .getAuthorizationExchange() .getAuthorizationRequest() @@ -170,85 +137,6 @@ public Authentication doAuthenticate(Authentication authentication) throws Authe .getRedirectUri(); try { - //build oauth2 user service for this registration - //TODO cache by registrationId - DefaultOAuth2UserService oauth2UserService = new DefaultOAuth2UserService(); - String jwksUri = loginAuthenticationToken.getClientRegistration().getProviderDetails().getJwkSetUri(); - JWKSet jwks = null; - try { - //try to parse jwks from metadata - Object value = loginAuthenticationToken - .getClientRegistration() - .getProviderDetails() - .getConfigurationMetadata() - .get("jwks"); - - if (value instanceof JSONObject) { - jwks = JWKSet.parse(((JSONObject) value).toJSONString()); - } - } catch (ParseException e) { - logger.error("error reading jwks from metadata: " + e.getMessage(), e); - } - - JoseRestOperations restOperations = null; - - if (StringUtils.hasText(jwksUri)) { - //we expect a response encrypted with out public key, or just signed with op keys - restOperations = new JoseRestOperations(jwksUri); - if ( - config.getConfigMap().getUserInfoJWEAlg() != null && - config.getConfigMap().getUserInfoJWEEnc() != null - ) { - restOperations = - new JoseRestOperations( - jwksUri, - config.getClientEncryptionJWK(), - config.getConfigMap().getUserInfoJWEAlg().getValue(), - config.getConfigMap().getUserInfoJWEEnc().getValue() - ); - } - } - - if (!StringUtils.hasText(jwksUri) && jwks != null) { - //use jwks - restOperations = new JoseRestOperations(jwks); - if ( - config.getConfigMap().getUserInfoJWEAlg() != null && - config.getConfigMap().getUserInfoJWEEnc() != null - ) { - restOperations = - new JoseRestOperations( - jwks, - config.getClientEncryptionJWK(), - config.getConfigMap().getUserInfoJWEAlg().getValue(), - config.getConfigMap().getUserInfoJWEEnc().getValue() - ); - } - } - - if (restOperations == null) { - throw new OAuth2AuthenticationException("invalid_request"); - } - - oauth2UserService.setRestOperations(restOperations); - - //build oidc provider - OidcUserService oidcUserService = new OidcUserService(); - oidcUserService.setOauth2UserService(oauth2UserService); - //always load user profile - hack - //TODO evaluate if scopes OR claims are requested - oidcUserService.setAccessibleScopes(Collections.singleton("openid")); - OidcAuthorizationCodeAuthenticationProvider oidcProvider = new OidcAuthorizationCodeAuthenticationProvider( - accessTokenResponseClient, - oidcUserService - ); - // replace jwtDecoderFactory to support providers with jwks in place of jwksUri - oidcProvider.setJwtDecoderFactory(new OIDCIdTokenDecoderFactory()); - - // use a custom authorities mapper to cleanup authorities spring injects - // default impl translates the whole oauth response as an authority.. - oidcProvider.setAuthoritiesMapper(nullAuthoritiesMapper); - // delegate to oidc provider Authentication auth = oidcProvider.authenticate(authentication); diff --git a/src/main/java/it/smartcommunitylab/aac/openidfed/provider/OpenIdFedFilterProvider.java b/src/main/java/it/smartcommunitylab/aac/openidfed/provider/OpenIdFedFilterProvider.java index e7d8501c5..9b0ec78f2 100644 --- a/src/main/java/it/smartcommunitylab/aac/openidfed/provider/OpenIdFedFilterProvider.java +++ b/src/main/java/it/smartcommunitylab/aac/openidfed/provider/OpenIdFedFilterProvider.java @@ -31,19 +31,22 @@ import java.util.Collection; import java.util.List; import javax.servlet.Filter; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; import org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizationRequestRepository; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.util.Assert; -public class OpenIdFedFilterProvider implements FilterProvider { +public class OpenIdFedFilterProvider implements FilterProvider, ApplicationEventPublisherAware { private final String authorityId; private final ProviderConfigRepository registrationRepository; private AuthenticationManager authManager; private RealmAwareUriBuilder realmAwareUriBuilder; + private ApplicationEventPublisher eventPublisher; public OpenIdFedFilterProvider(ProviderConfigRepository registrationRepository) { this(SystemKeys.AUTHORITY_OPENIDFED, registrationRepository); @@ -68,6 +71,11 @@ public void setRealmAwareUriBuilder(RealmAwareUriBuilder realmAwareUriBuilder) { this.realmAwareUriBuilder = realmAwareUriBuilder; } + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + @Override public String getAuthorityId() { return authorityId; @@ -100,8 +108,9 @@ public List getAuthFilters() { OpenIdFedRedirectAuthenticationFilter redirectFilter = new OpenIdFedRedirectAuthenticationFilter( authorityId, registrationRepository, - buildFilterUrl("authorize") + buildFilterUrl("authorize/{providerId}") ); + redirectFilter.setApplicationEventPublisher(eventPublisher); redirectFilter.setAuthorizationRequestRepository(authorizationRequestRepository); OpenIdFedLoginAuthenticationFilter loginFilter = new OpenIdFedLoginAuthenticationFilter( @@ -110,6 +119,7 @@ public List getAuthFilters() { buildFilterUrl("login/{providerId}"), null ); + loginFilter.setApplicationEventPublisher(eventPublisher); loginFilter.setAuthorizationRequestRepository(authorizationRequestRepository); if (authManager != null) { diff --git a/src/main/java/it/smartcommunitylab/aac/openidfed/provider/OpenIdFedIdentityProvider.java b/src/main/java/it/smartcommunitylab/aac/openidfed/provider/OpenIdFedIdentityProvider.java index 2fbbdb5c0..9686486e1 100644 --- a/src/main/java/it/smartcommunitylab/aac/openidfed/provider/OpenIdFedIdentityProvider.java +++ b/src/main/java/it/smartcommunitylab/aac/openidfed/provider/OpenIdFedIdentityProvider.java @@ -40,6 +40,7 @@ import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.util.StringUtils; public class OpenIdFedIdentityProvider @@ -114,6 +115,12 @@ public void setResourceService(ResourceEntityService resourceService) { this.accountService.setResourceService(resourceService); } + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + this.authenticationProvider.setApplicationEventPublisher(eventPublisher); + } + @Override public OpenIdFedAuthenticationProvider getAuthenticationProvider() { return authenticationProvider; diff --git a/src/main/java/it/smartcommunitylab/aac/openidfed/service/OpenIdFedOidcUserService.java b/src/main/java/it/smartcommunitylab/aac/openidfed/service/OpenIdFedOidcUserService.java new file mode 100644 index 000000000..e70b16ea0 --- /dev/null +++ b/src/main/java/it/smartcommunitylab/aac/openidfed/service/OpenIdFedOidcUserService.java @@ -0,0 +1,183 @@ +/** + * Copyright 2024 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package it.smartcommunitylab.aac.openidfed.service; + +import com.nimbusds.jose.jwk.JWKSet; +import it.smartcommunitylab.aac.jwt.JoseRestOperations; +import it.smartcommunitylab.aac.oidc.events.OAuth2UserResponseEvent; +import it.smartcommunitylab.aac.openidfed.auth.OpenIdFedUserRequestEntityConverter; +import it.smartcommunitylab.aac.openidfed.provider.OpenIdFedIdentityProviderConfig; +import java.text.ParseException; +import java.util.Collections; +import java.util.Map; +import net.minidev.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.converter.ClaimTypeConverter; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +public class OpenIdFedOidcUserService extends OidcUserService implements ApplicationEventPublisherAware { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private final OpenIdFedIdentityProviderConfig config; + + private DefaultOAuth2UserService oauth2UserService; + private AuditAwareClaimTypeConverter claimTypeConverter; + private OpenIdFedUserRequestEntityConverter userRequestEntityConverter; + + public OpenIdFedOidcUserService(OpenIdFedIdentityProviderConfig config) { + Assert.notNull(config, "provider config is required"); + this.config = config; + + //configure oauth2 user service + oauth2UserService = new DefaultOAuth2UserService(); + userRequestEntityConverter = new OpenIdFedUserRequestEntityConverter(config); + oauth2UserService.setRequestEntityConverter(userRequestEntityConverter); + + super.setOauth2UserService(oauth2UserService); + + //instrument a custom claim converter to audit response + //hack + claimTypeConverter = + new AuditAwareClaimTypeConverter(new ClaimTypeConverter(createDefaultClaimTypeConverters())); + + super.setClaimTypeConverterFactory(clientRegistration -> claimTypeConverter); + + //always load user profile - hack + //TODO add method to evaluate if scopes OR claims are requested + setAccessibleScopes(Collections.singleton("openid")); + } + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) { + userRequestEntityConverter.setApplicationEventPublisher(eventPublisher); + claimTypeConverter.setApplicationEventPublisher(eventPublisher); + } + + @Override + public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException { + Assert.notNull(userRequest, "userRequest cannot be null"); + Assert.notNull(userRequest.getClientRegistration(), "clientRegistration cannot be null"); + + //rebuild restTemplate for the provider configuration + String jwksUri = userRequest.getClientRegistration().getProviderDetails().getJwkSetUri(); + JWKSet jwks = null; + try { + //try to parse jwks from metadata + Object value = userRequest + .getClientRegistration() + .getProviderDetails() + .getConfigurationMetadata() + .get("jwks"); + + if (value instanceof JSONObject) { + jwks = JWKSet.parse(((JSONObject) value).toJSONString()); + } + } catch (ParseException e) { + logger.error("error reading jwks from metadata: " + e.getMessage(), e); + } + + JoseRestOperations restOperations = null; + + if (StringUtils.hasText(jwksUri)) { + //we expect a response encrypted with out public key, or just signed with op keys + restOperations = new JoseRestOperations(jwksUri); + if ( + config.getConfigMap().getUserInfoJWEAlg() != null && config.getConfigMap().getUserInfoJWEEnc() != null + ) { + restOperations = + new JoseRestOperations( + jwksUri, + config.getClientEncryptionJWK(), + config.getConfigMap().getUserInfoJWEAlg().getValue(), + config.getConfigMap().getUserInfoJWEEnc().getValue() + ); + } + } + + if (!StringUtils.hasText(jwksUri) && jwks != null) { + //use jwks + restOperations = new JoseRestOperations(jwks); + if ( + config.getConfigMap().getUserInfoJWEAlg() != null && config.getConfigMap().getUserInfoJWEEnc() != null + ) { + restOperations = + new JoseRestOperations( + jwks, + config.getClientEncryptionJWK(), + config.getConfigMap().getUserInfoJWEAlg().getValue(), + config.getConfigMap().getUserInfoJWEEnc().getValue() + ); + } + } + + if (restOperations == null) { + throw new OAuth2AuthenticationException("invalid_request"); + } + + //set error handler + restOperations.setErrorHandler(new OAuth2ErrorResponseErrorHandler()); + + oauth2UserService.setRestOperations(restOperations); + + //delegate + return super.loadUser(userRequest); + } + + private class AuditAwareClaimTypeConverter + implements Converter, Map>, ApplicationEventPublisherAware { + + private ClaimTypeConverter claimTypeConverter; + private ApplicationEventPublisher eventPublisher; + + public AuditAwareClaimTypeConverter(ClaimTypeConverter claimTypeConverter) { + Assert.notNull(claimTypeConverter, "claim type converter can not be null"); + this.claimTypeConverter = claimTypeConverter; + } + + public Map convert(Map claims) { + if (eventPublisher != null) { + OAuth2UserResponseEvent event = new OAuth2UserResponseEvent( + config.getAuthority(), + config.getProvider(), + config.getRealm(), + claims + ); + + eventPublisher.publishEvent(event); + } + + return claimTypeConverter.convert(claims); + } + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + } +} diff --git a/src/main/java/it/smartcommunitylab/aac/saml/SamlIdentityAuthority.java b/src/main/java/it/smartcommunitylab/aac/saml/SamlIdentityAuthority.java index 9053eaf68..aaf29f865 100644 --- a/src/main/java/it/smartcommunitylab/aac/saml/SamlIdentityAuthority.java +++ b/src/main/java/it/smartcommunitylab/aac/saml/SamlIdentityAuthority.java @@ -31,12 +31,15 @@ import it.smartcommunitylab.aac.saml.provider.SamlIdentityProviderConfig; import it.smartcommunitylab.aac.saml.provider.SamlIdentityProviderConfigMap; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.stereotype.Service; import org.springframework.util.Assert; @Service public class SamlIdentityAuthority - extends AbstractIdentityProviderAuthority { + extends AbstractIdentityProviderAuthority + implements ApplicationEventPublisherAware { public static final String AUTHORITY_URL = "/auth/" + SystemKeys.AUTHORITY_SAML + "/"; @@ -93,6 +96,11 @@ public void setResourceService(ResourceEntityService resourceService) { this.resourceService = resourceService; } + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) { + this.filterProvider.setApplicationEventPublisher(eventPublisher); + } + @Override public void afterPropertiesSet() throws Exception { super.afterPropertiesSet(); diff --git a/src/main/java/it/smartcommunitylab/aac/saml/audit/SamlAuditEventListener.java b/src/main/java/it/smartcommunitylab/aac/saml/audit/SamlAuditEventListener.java new file mode 100644 index 000000000..fc841128c --- /dev/null +++ b/src/main/java/it/smartcommunitylab/aac/saml/audit/SamlAuditEventListener.java @@ -0,0 +1,126 @@ +/** + * Copyright 2024 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package it.smartcommunitylab.aac.saml.audit; + +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import it.smartcommunitylab.aac.SystemKeys; +import it.smartcommunitylab.aac.audit.store.AuditApplicationEventMixIns; +import it.smartcommunitylab.aac.core.provider.ProviderConfigRepository; +import it.smartcommunitylab.aac.saml.events.SamlRequestEvent; +import it.smartcommunitylab.aac.saml.provider.SamlIdentityProviderConfig; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.audit.AuditEvent; +import org.springframework.boot.actuate.audit.listener.AuditApplicationEvent; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +@Component +public class SamlAuditEventListener implements ApplicationListener, ApplicationEventPublisherAware { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private static final String SAML_REQUEST = "SAML_REQUEST"; + + private final ObjectMapper mapper = new ObjectMapper() + .registerModule(new JavaTimeModule()) + //include only non-null fields + .setSerializationInclusion(Include.NON_NULL) + //add mixin for including typeInfo in events + .addMixIn(ApplicationEvent.class, AuditApplicationEventMixIns.class); + private final TypeReference> typeRef = new TypeReference>() {}; + + private ApplicationEventPublisher publisher; + + private ProviderConfigRepository registrationRepository; + + @Autowired + public void setRegistrationRepository(ProviderConfigRepository registrationRepository) { + this.registrationRepository = registrationRepository; + } + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher publisher) { + this.publisher = publisher; + } + + @Override + public void onApplicationEvent(SamlRequestEvent event) { + String authority = event.getAuthority(); + String provider = event.getProvider(); + String realm = event.getRealm(); + String relayState = event.getRelayState(); + + logger.debug("receive saml request event for {}:{} key {}", authority, provider, String.valueOf(relayState)); + + if (registrationRepository == null || publisher == null) { + logger.debug("invalid configuration, skip event"); + return; + } + + SamlIdentityProviderConfig config = registrationRepository.findByProviderId(provider); + if (config == null) { + logger.debug("missing provider configuration, skip event"); + return; + } + + String level = config.getSettingsMap().getEvents(); + if (SystemKeys.EVENTS_LEVEL_NONE.equals(level)) { + logger.debug("provider configuration level none, skip event"); + return; + } + + Map data = new HashMap<>(); + data.put("authority", authority); + data.put("provider", provider); + data.put("realm", realm); + + if (StringUtils.hasText(relayState)) { + data.put("tx", relayState); + } + + if (SystemKeys.EVENTS_LEVEL_FULL.equals(level)) { + //serialize to avoid exposing object to audit + data.put("event", mapper.convertValue(event, typeRef)); + } + + AuditApplicationEvent auditEvent = new AuditApplicationEvent( + Instant.ofEpochMilli(event.getTimestamp()), + provider, + SAML_REQUEST, + data + ); + + logger.debug("publish saml request event for audit"); + if (logger.isTraceEnabled()) { + logger.trace("audit event: {}", auditEvent); + } + + publisher.publishEvent(auditEvent); + } +} diff --git a/src/main/java/it/smartcommunitylab/aac/saml/auth/SamlUserAuthenticationFailureEvent.java b/src/main/java/it/smartcommunitylab/aac/saml/auth/SamlUserAuthenticationFailureEvent.java index e3b8503a3..39612a976 100644 --- a/src/main/java/it/smartcommunitylab/aac/saml/auth/SamlUserAuthenticationFailureEvent.java +++ b/src/main/java/it/smartcommunitylab/aac/saml/auth/SamlUserAuthenticationFailureEvent.java @@ -17,7 +17,7 @@ package it.smartcommunitylab.aac.saml.auth; import it.smartcommunitylab.aac.SystemKeys; -import it.smartcommunitylab.aac.audit.UserAuthenticationFailureEvent; +import it.smartcommunitylab.aac.events.UserAuthenticationFailureEvent; import java.io.Serializable; import java.util.Map; import org.springframework.security.core.Authentication; diff --git a/src/main/java/it/smartcommunitylab/aac/saml/auth/SamlWebSsoAuthenticationRequestFilter.java b/src/main/java/it/smartcommunitylab/aac/saml/auth/SamlWebSsoAuthenticationRequestFilter.java index e9efad671..8be8c2f7a 100644 --- a/src/main/java/it/smartcommunitylab/aac/saml/auth/SamlWebSsoAuthenticationRequestFilter.java +++ b/src/main/java/it/smartcommunitylab/aac/saml/auth/SamlWebSsoAuthenticationRequestFilter.java @@ -19,6 +19,7 @@ import it.smartcommunitylab.aac.SystemKeys; import it.smartcommunitylab.aac.core.provider.ProviderConfigRepository; import it.smartcommunitylab.aac.saml.SamlIdentityAuthority; +import it.smartcommunitylab.aac.saml.events.SamlAuthenticationRequestEvent; import it.smartcommunitylab.aac.saml.provider.SamlIdentityProviderConfig; import it.smartcommunitylab.aac.saml.service.HttpSessionSaml2AuthenticationRequestRepository; import java.io.IOException; @@ -30,6 +31,8 @@ import javax.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.core.convert.converter.Converter; import org.springframework.http.MediaType; import org.springframework.lang.Nullable; @@ -50,7 +53,9 @@ import org.springframework.web.util.UriComponentsBuilder; import org.springframework.web.util.UriUtils; -public class SamlWebSsoAuthenticationRequestFilter extends OncePerRequestFilter { +public class SamlWebSsoAuthenticationRequestFilter + extends OncePerRequestFilter + implements ApplicationEventPublisherAware { private final Logger logger = LoggerFactory.getLogger(getClass()); @@ -62,11 +67,13 @@ public class SamlWebSsoAuthenticationRequestFilter extends OncePerRequestFilter private final RequestMatcher requestMatcher; private final Saml2AuthenticationRequestContextResolver authenticationRequestContextResolver; private final Saml2AuthenticationRequestFactory authenticationRequestFactory; - // private final ProviderRepository registrationRepository; + private final ProviderConfigRepository registrationRepository; private Saml2AuthenticationRequestRepository authenticationRequestRepository = new HttpSessionSaml2AuthenticationRequestRepository(); + private ApplicationEventPublisher eventPublisher; + public SamlWebSsoAuthenticationRequestFilter( ProviderConfigRepository registrationRepository, RelyingPartyRegistrationRepository relyingPartyRegistrationRepository @@ -84,7 +91,7 @@ public SamlWebSsoAuthenticationRequestFilter( Assert.notNull(registrationRepository, "registration repository cannot be null"); Assert.notNull(relyingPartyRegistrationRepository, "relying party repository cannot be null"); - // this.registrationRepository = registrationRepository; + this.registrationRepository = registrationRepository; this.authorityId = authority; // use custom implementation to add secure relayState param @@ -103,6 +110,11 @@ protected String getFilterName() { return getClass().getName() + "." + authorityId; } + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { @@ -119,6 +131,15 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse return; } + //resolve provider + String registrationId = context.getRelyingPartyRegistration().getRegistrationId(); + SamlIdentityProviderConfig config = registrationRepository.findByProviderId(registrationId); + if (config == null) { + logger.error("error retrieving provider for registration {}", String.valueOf(registrationId)); + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + return; + } + // translate context to a serializable version // TODO drop and adopt resolver+request as per spring 5.6+ AbstractSaml2AuthenticationRequest authenticationRequest = resolve(context); @@ -143,6 +164,17 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse authenticationRequestRepository.saveAuthenticationRequest(ctx, request, response); } + if (eventPublisher != null) { + eventPublisher.publishEvent( + new SamlAuthenticationRequestEvent( + authorityId, + config.getProvider(), + config.getRealm(), + authenticationRequest + ) + ); + } + if (authenticationRequest instanceof Saml2RedirectAuthenticationRequest) { sendRedirect(response, (Saml2RedirectAuthenticationRequest) authenticationRequest); } else { diff --git a/src/main/java/it/smartcommunitylab/aac/saml/events/SamlAuthenticationRequestEvent.java b/src/main/java/it/smartcommunitylab/aac/saml/events/SamlAuthenticationRequestEvent.java new file mode 100644 index 000000000..ddcc9f152 --- /dev/null +++ b/src/main/java/it/smartcommunitylab/aac/saml/events/SamlAuthenticationRequestEvent.java @@ -0,0 +1,42 @@ +/** + * Copyright 2024 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package it.smartcommunitylab.aac.saml.events; + +import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest; +import org.springframework.util.Assert; + +public class SamlAuthenticationRequestEvent extends SamlRequestEvent { + + public SamlAuthenticationRequestEvent( + String authority, + String provider, + String realm, + AbstractSaml2AuthenticationRequest request + ) { + super(authority, provider, realm, request); + Assert.notNull(request, "request can not be null"); + } + + public AbstractSaml2AuthenticationRequest getAuthenticationRequest() { + return (AbstractSaml2AuthenticationRequest) super.getSource(); + } + + @Override + public String getRelayState() { + return getAuthenticationRequest().getRelayState(); + } +} diff --git a/src/main/java/it/smartcommunitylab/aac/saml/events/SamlRequestEvent.java b/src/main/java/it/smartcommunitylab/aac/saml/events/SamlRequestEvent.java new file mode 100644 index 000000000..039e3413b --- /dev/null +++ b/src/main/java/it/smartcommunitylab/aac/saml/events/SamlRequestEvent.java @@ -0,0 +1,60 @@ +/** + * Copyright 2024 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package it.smartcommunitylab.aac.saml.events; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import it.smartcommunitylab.aac.events.ProviderEmittedEvent; +import org.springframework.context.ApplicationEvent; +import org.springframework.util.Assert; + +public abstract class SamlRequestEvent extends ApplicationEvent implements ProviderEmittedEvent { + + private final String authority; + private final String provider; + private final String realm; + + protected SamlRequestEvent(String authority, String provider, String realm, Object source) { + super(source); + Assert.hasText(provider, "provider identifier can not be null or blank"); + this.authority = authority; + this.provider = provider; + this.realm = realm; + } + + public String getAuthority() { + return authority; + } + + public String getProvider() { + return provider; + } + + public String getRealm() { + return realm; + } + + @JsonInclude(Include.NON_NULL) + public abstract String getRelayState(); + + @JsonIgnore + @Override + public Object getSource() { + return super.getSource(); + } +} diff --git a/src/main/java/it/smartcommunitylab/aac/saml/provider/SamlFilterProvider.java b/src/main/java/it/smartcommunitylab/aac/saml/provider/SamlFilterProvider.java index 8e0ae7d74..78164ba47 100644 --- a/src/main/java/it/smartcommunitylab/aac/saml/provider/SamlFilterProvider.java +++ b/src/main/java/it/smartcommunitylab/aac/saml/provider/SamlFilterProvider.java @@ -31,11 +31,13 @@ import java.util.List; import java.util.stream.Collectors; import javax.servlet.Filter; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationRequestContext; import org.springframework.util.Assert; -public class SamlFilterProvider implements FilterProvider { +public class SamlFilterProvider implements FilterProvider, ApplicationEventPublisherAware { private final String authorityId; @@ -43,6 +45,7 @@ public class SamlFilterProvider implements FilterProvider { private final SamlRelyingPartyRegistrationRepository relyingPartyRegistrationRepository; private AuthenticationManager authManager; + private ApplicationEventPublisher eventPublisher; public SamlFilterProvider( String authorityId, @@ -67,6 +70,11 @@ public String getAuthorityId() { return authorityId; } + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + @Override public List getAuthFilters() { // build request repository bound to session @@ -80,6 +88,7 @@ public List getAuthFilters() { relyingPartyRegistrationRepository, buildFilterUrl("authenticate/{registrationId}") ); + requestFilter.setApplicationEventPublisher(eventPublisher); requestFilter.setAuthenticationRequestRepository(authenticationRequestRepository); SamlWebSsoAuthenticationFilter ssoFilter = new SamlWebSsoAuthenticationFilter( diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index a2e40bb85..c84dbdc75 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -95,6 +95,13 @@ jwt: includeInternalRoles: false includeResourceRoles: false +# AUDIT +audit: + issuer: ${AUDIT_ISSUER:${application.url}} + kid: + sig: ${AUDIT_KID_SIG:} + enc: ${AUDIT_KID_ENC:} + #EXTERNAL PROVIDERS authorities: account: diff --git a/src/main/resources/db/sql/audit/schema-h2.sql b/src/main/resources/db/sql/audit/schema-h2.sql index 1b16f5755..d54daa7cc 100644 --- a/src/main/resources/db/sql/audit/schema-h2.sql +++ b/src/main/resources/db/sql/audit/schema-h2.sql @@ -3,6 +3,8 @@ CREATE TABLE event_time TIMESTAMP, principal varchar(255), realm varchar(255) DEFAULT NULL, + tx varchar(255), event_type varchar(255), + event_class varchar(255), event_data BLOB - ); + ); \ No newline at end of file diff --git a/src/main/resources/db/sql/audit/schema-mysql.sql b/src/main/resources/db/sql/audit/schema-mysql.sql index 524972960..5c184f2bd 100644 --- a/src/main/resources/db/sql/audit/schema-mysql.sql +++ b/src/main/resources/db/sql/audit/schema-mysql.sql @@ -3,10 +3,16 @@ CREATE TABLE event_time TIMESTAMP, principal varchar(255), realm varchar(255) DEFAULT NULL, + tx varchar(255), event_type varchar(255), + event_class varchar(255), event_data BLOB ) ENGINE = InnoDB ROW_FORMAT = DYNAMIC; CREATE INDEX audit_ix1 ON audit_events (principal); -CREATE INDEX audit_ix2 ON audit_events (realm); \ No newline at end of file +CREATE INDEX audit_ix2 ON audit_events (realm); + +CREATE INDEX audit_ix3 ON audit_events (tx); + +CREATE INDEX audit_ix4 ON audit_events (event_class); \ No newline at end of file diff --git a/src/main/resources/db/sql/audit/schema-postgresql.sql b/src/main/resources/db/sql/audit/schema-postgresql.sql index d07479b64..dd7285712 100644 --- a/src/main/resources/db/sql/audit/schema-postgresql.sql +++ b/src/main/resources/db/sql/audit/schema-postgresql.sql @@ -3,10 +3,16 @@ CREATE TABLE event_time TIMESTAMP, principal varchar(255), realm varchar(255) DEFAULT NULL, + tx varchar(255), event_type varchar(255), + event_class varchar(255), event_data bytea ); CREATE INDEX audit_ix1 ON audit_events (principal); -CREATE INDEX audit_ix2 ON audit_events (realm); \ No newline at end of file +CREATE INDEX audit_ix2 ON audit_events (realm); + +CREATE INDEX audit_ix3 ON audit_events (tx); + +CREATE INDEX audit_ix4 ON audit_events (event_class); \ No newline at end of file