From 880da07a84c306a86bbb570920c01b02159a76ef Mon Sep 17 00:00:00 2001 From: "Brian M. Long" Date: Mon, 19 May 2025 09:44:23 -0400 Subject: [PATCH] added /app/rest support for non-UI requests --- .../IdentityServiceConfigurationOverride.java | 58 +++++++++++++-- .../keycloak-import/realm-my-app.json | 73 +++++++++++++++++-- src/test/vscode/simple.http | 35 +++++++++ 3 files changed, 155 insertions(+), 11 deletions(-) create mode 100644 src/test/vscode/simple.http diff --git a/src/main/java/com/inteligr8/activiti/auth/oauth/IdentityServiceConfigurationOverride.java b/src/main/java/com/inteligr8/activiti/auth/oauth/IdentityServiceConfigurationOverride.java index b1fc0f6..94470ea 100644 --- a/src/main/java/com/inteligr8/activiti/auth/oauth/IdentityServiceConfigurationOverride.java +++ b/src/main/java/com/inteligr8/activiti/auth/oauth/IdentityServiceConfigurationOverride.java @@ -1,5 +1,6 @@ package com.inteligr8.activiti.auth.oauth; +import static org.springframework.security.config.Customizer.withDefaults; import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher; import org.apache.commons.lang3.StringUtils; @@ -14,11 +15,17 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.core.annotation.Order; import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.util.matcher.AndRequestMatcher; +import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher; +import com.activiti.domain.idm.Capabilities; +import com.activiti.security.ActivitiAppRequestHeaderService; +import com.activiti.security.ActivitiRestAuthorizationService; import com.activiti.security.ProtectedPaths; import com.activiti.security.identity.service.config.IdentityServiceEnabledCondition; import com.inteligr8.activiti.auth.service.JwtAuthenticationProvider; @@ -43,6 +50,12 @@ public class IdentityServiceConfigurationOverride { @Autowired private JwtAuthenticationProvider jwtAuthenticationProvider; + @Autowired + private ActivitiAppRequestHeaderService appRequestHeaderService; + + @Autowired + private ActivitiRestAuthorizationService restAuthorizationService; + @Bean("inteligr8.clientRegistrationRepository") @Primary public ClientRegistrationRepository clientRegistrationRepository() { @@ -73,11 +86,13 @@ public class IdentityServiceConfigurationOverride { } /** - * Slightly lower priority than the one provided OOTB. This - * allows for the bean injection of the JwtAuthenticationConverter. + * Slightly higher priority than the one provided OOTB. This allows for + * the bean injection of the `JwtAuthenticationConverter`. * - * A lower priority means it is applied last. This means it replaces the - * JwtAuthenticationConverter provided by Alfresco OOTB. + * This is basically a copy of what is provided OOTB, but: + * + * - The ability to configure the `JwtAuthenticationConverter`. + * - Allow non-UI access to `/app/rest/*` * * @see com.activiti.security.identity.service.config.IdentityServiceConfigurationApi#identityServiceApiWebSecurity */ @@ -85,11 +100,44 @@ public class IdentityServiceConfigurationOverride { @Order(-5) public SecurityFilterChain identityServiceApiWebSecurity(HttpSecurity http) throws Exception { http - .securityMatcher(antMatcher(ProtectedPaths.API_URL_PATH + "/**")) + .securityMatchers(matchers -> { + matchers.requestMatchers( + // same as OOTB + antMatcher(ProtectedPaths.API_URL_PATH + "/**"), + + // want to also allow non-UI access to the the protected API + // we do this for anything with an `Authorization` header, as the UI uses session-based authorization + new AndRequestMatcher(new RequestHeaderRequestMatcher("Authorization"), antMatcher(ProtectedPaths.APP_URL_PATH + "/rest/**")) + ); + }) + .csrf(csrf -> { + csrf.disable(); + }) + .cors(withDefaults()) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // Stores no Session for API calls .oauth2ResourceServer(oauth2 -> oauth2.jwt(jwtConfigurer -> { + // here is where we are injecting a Spring extendible `JwtAuthenticationConverter`. jwtConfigurer.jwtAuthenticationConverter(this.jwtAuthenticationProvider.create()); }) + ) + .authorizeHttpRequests(request -> + request + // same as OOTB + .requestMatchers(antMatcher(ProtectedPaths.API_URL_PATH + "/enterprise/**")) + .access(this.appRequestHeaderService) + .requestMatchers(antMatcher(ProtectedPaths.API_URL_PATH + "/**")) + .access(this.restAuthorizationService) + + // borrowed from OOTB /app/rest security + .requestMatchers(antMatcher(ProtectedPaths.APP_URL_PATH + "/rest/reporting/**")) + .hasAuthority(Capabilities.ACCESS_REPORTS) + + .requestMatchers( + antMatcher(ProtectedPaths.API_URL_PATH + "/**"), + antMatcher(ProtectedPaths.APP_URL_PATH + "/rest/**") + ) + .authenticated() ); return http.build(); diff --git a/src/test/resources/keycloak-import/realm-my-app.json b/src/test/resources/keycloak-import/realm-my-app.json index 823718b..d1c02ab 100644 --- a/src/test/resources/keycloak-import/realm-my-app.json +++ b/src/test/resources/keycloak-import/realm-my-app.json @@ -101,14 +101,14 @@ "profile", "roles", "basic", - "email" + "email", + "microprofile-jwt" ], "optionalClientScopes": [ "address", "phone", "organization", - "offline_access", - "microprofile-jwt" + "offline_access" ] }, { @@ -156,15 +156,76 @@ "profile", "roles", "basic", - "email" + "email", + "microprofile-jwt" ], "optionalClientScopes": [ "address", "phone", "organization", - "offline_access", - "microprofile-jwt" + "offline_access" ] + }, + { + "clientId": "cli", + "name": "Command Line Tools", + "description": "", + "rootUrl": "", + "adminUrl": "", + "baseUrl": "", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "eJa5W7bv4ohFbr7QRtaCk0eccRFoYM5x", + "redirectUris": [ + "/*" + ], + "webOrigins": [ + "/*" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": true, + "publicClient": false, + "frontchannelLogout": true, + "protocol": "openid-connect", + "attributes": { + "realm_client": "false", + "oidc.ciba.grant.enabled": "false", + "client.secret.creation.time": "1747506410", + "backchannel.logout.session.required": "true", + "standard.token.exchange.enabled": "true", + "oauth2.device.authorization.grant.enabled": "false", + "backchannel.logout.revoke.offline.tokens": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "basic", + "email", + "microprofile-jwt" + ], + "optionalClientScopes": [ + "address", + "phone", + "organization", + "offline_access" + ], + "access": { + "view": true, + "configure": true, + "manage": true + } } ], "users": [ diff --git a/src/test/vscode/simple.http b/src/test/vscode/simple.http new file mode 100644 index 0000000..1bd27ad --- /dev/null +++ b/src/test/vscode/simple.http @@ -0,0 +1,35 @@ +@keycloakRealm = my-app +@keycloakBaseUrl = http://localhost:8081 +@oauthUrl = {{keycloakBaseUrl}}/realms/{{keycloakRealm}} +@keycloakTokenUrl = {{oauthUrl}}/protocol/openid-connect/token +@oauthClientId = cli +@oauthClientSecret = eJa5W7bv4ohFbr7QRtaCk0eccRFoYM5x +@apsBaseUrl = http://localhost:8080/activiti-app + +### Token +# @name token +curl -LX POST {{keycloakTokenUrl}} \ + -H 'Content-type: application/x-www-form-urlencoded' \ + -d "grant_type=client_credentials" \ + -d "client_id={{oauthClientId}}" \ + -d "client_secret={{oauthClientSecret}}" + +@accessToken = {{token.response.body.access_token}} +@auth = Bearer {{accessToken}} + +### APS Version +# @name version +GET {{apsBaseUrl}}/api/enterprise/app-version +Authorization: Bearer {{accessToken}} + +### APS Tenants +# @name tenants +GET {{apsBaseUrl}}/api/enterprise/admin/tenants +Authorization: Bearer {{accessToken}} + +@tenantId = {{tenants.response.body.0.id}} + +### APS Templates +# @name templates +GET {{apsBaseUrl}}/app/rest/document-templates?tenantId={{tenantId}}&start=0&size=10&sort=sort_by_name_asc +Authorization: Bearer {{accessToken}}