Sets up XSUAA authentication and Application Router for Java apps migrating from SAP Neo to Cloud Foundry. Detects FORM auth in web.xml, security constraints, or UserProvider in Java code.
npx claudepluginhub sap-samples/btp-neo-java-app-migration --plugin sap-btp-neo-migrationThis skill is limited to using the following tools:
Set up XSUAA-based authentication and Application Router for Cloud Foundry.
Guides Next.js Cache Components and Partial Prerendering (PPR): 'use cache' directives, cacheLife(), cacheTag(), revalidateTag() for caching, invalidation, static/dynamic optimization. Auto-activates on cacheComponents: true.
Processes PDFs: extracts text/tables/images, merges/splits/rotates pages, adds watermarks, creates/fills forms, encrypts/decrypts, OCRs scans. Activates on PDF mentions or output requests.
Share bugs, ideas, or general feedback.
Set up XSUAA-based authentication and Application Router for Cloud Foundry.
Replace Neo's built-in FORM authentication and UserManagementAccessor with Cloud Foundry's XSUAA service and Application Router for secure web application access.
This skill applies if any of these patterns are found:
<auth-method>FORM</auth-method>
<!-- OR -->
<security-constraint>
<web-resource-collection>
<web-resource-name>Protected</web-resource-name>
<url-pattern>/protected/*</url-pattern>
</web-resource-collection>
</security-constraint>
<!-- OR -->
<security-role>
<role-name>Everyone</role-name>
</security-role>
import com.sap.security.um.user.UserProvider;
import com.sap.security.um.user.User;
// OR
request.getUserPrincipal();
request.isUserInRole("Everyone");
Working directory: This skill must run inside the
-cf-migrationcopy of your app, created byjakarta-java25-migrationorneo-to-cf-migration-orchestrator. If your current directory does not end in-cf-migration, switch to it before proceeding.
Before invoking this skill, ensure you have invoked:
Use the sdk-replacement skill
Create xs-security.json in your project root (or cf/ folder) - see assets/xs-security.json for template:
{
"xsappname": "${app-name}",
"tenant-mode": "dedicated",
"scopes": [
{
"name": "$XSAPPNAME.Everyone",
"description": "Everyone scope for authenticated users"
}
],
"role-templates": [
{
"name": "Everyone",
"scope-references": [
"$XSAPPNAME.Everyone"
]
}
],
"role-collections": [
{
"name": "${app-name}-Everyone",
"role-template-references": [
"$XSAPPNAME.Everyone"
]
}
]
}
Customize: Replace
${app-name}with your application name andEveryonewith each actual role name from your Neo app. The naming convention for role collections is<app-name>-<role-name>(e.g.myapp-Admin,myapp-Viewer). Add one entry per role-template in therole-collectionsarray.subaccount-roles-importreads these names to link deployed role-templates and assign users.
Before:
<login-config>
<auth-method>FORM</auth-method>
<form-login-config>
<form-login-page>/login.html</form-login-page>
<form-error-page>/login-error.html</form-error-page>
</form-login-config>
</login-config>
After:
<login-config>
<auth-method>XSUAA</auth-method>
</login-config>
Add to pom.xml:
<!-- SAP Cloud Security Java API (non-Spring applications) -->
<dependency>
<groupId>com.sap.cloud.security</groupId>
<artifactId>java-api</artifactId>
</dependency>
<!-- For Spring Boot applications, use instead:
<dependency>
<groupId>com.sap.cloud.security</groupId>
<artifactId>resourceserver-security-spring-boot-starter</artifactId>
</dependency>
-->
Note: The
java-apiartifact is managed by thecf-tomcat-bomBOM, so no version is needed.
Create approuter/ directory with these files:
Copy from assets/package.json:
{
"name": "approuter",
"dependencies": {
"@sap/approuter": "^16.0.0"
},
"scripts": {
"start": "node node_modules/@sap/approuter/approuter.js"
}
}
See assets/xs-app.json for a complete template:
{
"authenticationMethod": "route",
"routes": [
{
"source": "^/protected(/.*)?$",
"target": "/protected$1",
"destination": "backend-app-destination",
"authenticationType": "xsuaa",
"scope": "$XSAPPNAME.Everyone",
"csrfProtection": false
},
{
"source": "^(/.*)",
"target": "$1",
"destination": "backend-app-destination",
"authenticationType": "none",
"csrfProtection": false
}
],
"logout": {
"logoutEndpoint": "/logout",
"logoutPage": "/"
}
}
Scopes in routes: Use
"scope": "$XSAPPNAME.<ScopeName>"on each protected route to enforce XSUAA scope checks at the approuter level. The$XSAPPNAMEplaceholder is resolved at runtime to the bound XSUAA service'sxsappname. Routes without ascopefield only require authentication (whenauthenticationTypeisxsuaa).
Before (Neo):
import com.sap.security.um.user.UserProvider;
import com.sap.security.um.user.User;
@Resource
private UserProvider userProvider;
public void doGet(HttpServletRequest request, HttpServletResponse response) {
User user = userProvider.getUser(request);
String userName = user.getName();
boolean isAdmin = request.isUserInRole("Admin");
}
After (Cloud Foundry — in JAX-RS endpoints or servlets):
import com.sap.cloud.security.token.Token;
import com.sap.cloud.security.token.TokenClaims;
import jakarta.servlet.http.HttpServletRequest;
public void doGet(HttpServletRequest request, HttpServletResponse response) {
// Get user from JWT token
java.security.Principal principal = request.getUserPrincipal();
String userName = principal != null ? principal.getName() : "anonymous";
// Check role (scope)
boolean hasScope = request.isUserInRole("Everyone");
}
Create or update mtad.yaml:
_schema-version: "3.2"
version: 0.0.1
ID: ${app-name}
parameters:
enable-parallel-deployments: true
modules:
# Java Backend Application
- name: ${app-name}
type: java.tomcat
path: target/ROOT.war
parameters:
buildpack: sap_java_buildpack_jakarta
disk-quota: 1024M
memory: 1024M
properties:
ENABLE_SECURITY_JAVA_API_V2: true
JBP_CONFIG_COMPONENTS: "jres: ['com.sap.xs.java.buildpack.jdk.SAPMachineJDK']"
JBP_CONFIG_SAP_MACHINE_JDK: "{ version: 25.+ }"
TARGET_RUNTIME: tomcat
SET_LOGGING_LEVEL: 'ROOT: INFO'
requires:
- name: ${app-name}-xsuaa
- name: ${app-name}-destination
provides:
- name: ${app-name}-java-app
properties:
neo-app-url: '${default-url}'
# Application Router
- name: ${app-name}-approuter
type: nodejs
path: approuter
parameters:
disk-quota: 256M
memory: 256M
routes:
- route: '${protocol}://${app-name}.${default-domain}'
protocol: http1
properties:
XS_APP_LOG_LEVEL: debug
TENANT_HOST_PATTERN: '(.*).cfapps.sap.hana.ondemand.com'
CF_NODEJS_LOGGING_LEVEL: "info"
requires:
- name: ${app-name}-xsuaa
- name: ${app-name}-java-app
group: destinations
properties:
name: backend-app-destination
url: '~{neo-app-url}'
forwardAuthToken: true
resources:
# XSUAA Service
- name: ${app-name}-xsuaa
type: org.cloudfoundry.managed-service
parameters:
service: xsuaa
service-plan: application
path: ./xs-security.json
# Destination Service
- name: ${app-name}-destination
type: org.cloudfoundry.managed-service
parameters:
service: destination
service-plan: lite
Key points:
path: target/ROOT.war— WAR must be named ROOT so Tomcat serves at/. Configuremaven-war-pluginwith<warName>ROOT</warName>.ENABLE_SECURITY_JAVA_API_V2: true— required for XSUAA JWT validation via thejava-apilibrary.JBP_CONFIG_COMPONENTS+JBP_CONFIG_SAP_MACHINE_JDK— pin to SAPMachineJDK 17.provideson the backend uses a custom property name (e.g.neo-app-url) and the approuterrequiresreferences it with~{neo-app-url}. Theurlshorthand only works if theprovidesblock uses a property literally namedurl.disk-quota: 1024Mminimum — 512M causes deployment failures with the SAP Java buildpack.
If you had custom login pages for FORM authentication, you can remove them as the Approuter handles authentication via redirect to the identity provider.
Files to consider removing:
login.htmllogin-error.html| File | Location | Purpose |
|---|---|---|
xs-security.json | Project root | XSUAA security configuration |
package.json | approuter/ | Approuter Node.js dependencies |
xs-app.json | approuter/ | Approuter routing configuration |
| Service | Plan | Purpose |
|---|---|---|
xsuaa | application | OAuth 2.0 authorization server |
destination | lite | Internal routing (for approuter) |
mvn clean package
cf deploy . -f
cf services
# Should show xsuaa and destination services bound
https://${app-name}.${domain}Add debug endpoint to verify JWT token is received:
@WebServlet("/debug/token")
public class TokenDebugServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
Principal principal = req.getUserPrincipal();
resp.getWriter().println("User: " + (principal != null ? principal.getName() : "null"));
resp.getWriter().println("Is Everyone: " + req.isUserInRole("Everyone"));
}
}
Cause: Role collection not assigned to user. Solution: In BTP Cockpit, assign the role collection to your user.
Cause: Backend app not reachable. Solution: Check that the backend URL in provides/requires is correct.
Cause: WAR filename is not ROOT.war. The SAP Java buildpack deploys auth.war under the /auth context path. Requests to / or /currentuser return 404 because Tomcat only serves at /auth/*.
Solution: In pom.xml, set <warName>ROOT</warName> in the maven-war-plugin configuration:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.4.0</version>
<configuration>
<warName>ROOT</warName>
</configuration>
</plugin>
Update mtad.yaml path: to target/ROOT.war. Rebuild and redeploy.
Note:
<finalName>in<build>does NOT control the WAR filename — only<warName>in the plugin configuration does.
Solution: Add CORS configuration to approuter or backend:
// xs-app.json
{
"cors": {
"allowedOrigins": ["*"],
"allowedMethods": ["GET", "POST", "PUT", "DELETE"]
}
}
For scenarios requiring Basic Authentication (e.g., API access), see references/extended-approuter.md for an extended approuter implementation.
Cause: The XSSecurityAuthenticator Catalina valve only validates JWT tokens for URLs that match a <security-constraint> in web.xml. If the REST API URL patterns (e.g., /rest/*, /api/*) are not covered by any security constraint, the valve skips JWT validation entirely. The approuter forwards a valid JWT token in the Authorization header, but the backend never processes it. As a result, getUserPrincipal() returns null and all isUserInRole() calls return false — regardless of whether you use @Context, @Inject, or SessionContext. This is the most common cause of authorization failures after migration and is easy to miss because the Neo-era constraints often covered only static pages (e.g., /index.html) while REST APIs were covered by auth-method-specific URL prefixes (/s/api/*, /b/api/*) that were removed during migration.
Diagnosis: Add a debug endpoint and check whether getUserPrincipal() returns null. If it does, the issue is missing security constraints, not the injection method.
Solution: See Step 5 above. Add a <security-constraint> covering /rest/* (or /* for all paths) with an <auth-constraint> requiring the Everyone role. Fine-grained role checks (admin, manager) should be done in Java code, not via URL-level constraints.
Cause: First verify this is not the missing security constraint issue above (check if getUserPrincipal() returns null). If the principal IS set but isUserInRole() still returns false: @Context HttpServletRequest (JAX-RS injection) does not carry the XSUAA security context when used in @Stateless or @Singleton EJBs that are not JAX-RS resources. The request object is a CXF-internal wrapper that doesn't delegate isUserInRole() to the Catalina/XSUAA security realm. SessionContext.isCallerInRole() also fails because TomEE's OpenEJBSecurityListener does not fully propagate XSUAA roles to the OpenEJB security context. This is commonly seen in CDI producer beans or service provider EJBs that check roles before returning a service implementation.
Solution: See Step 7 above (TomEE variant). Replace @Context with @Inject for HttpServletRequest. CDI injection provides a request-scoped proxy that delegates to the real Catalina request with the XSUAA security context. Both isUserInRole() and getUserPrincipal() then work correctly. See also the tomee-runtime skill for details.
After completing this skill, proceed to: