- name
- authentication-xsuaa
- description
- Invoke this skill to set up XSUAA authentication and Application Router. Detects <auth-method>FORM</auth-method> in web.xml, security-constraint definitions, or UserProvider usage in Java code. Replaces Neo built-in auth with CF XSUAA.
- disable-model-invocation
- false
- allowed-tools
- Read, Edit, Write, Bash, Grep, Glob
# Authentication and Authorization
Set up XSUAA-based authentication and Application Router for Cloud Foundry.
## Purpose
Replace Neo's built-in FORM authentication and `UserManagementAccessor` with Cloud Foundry's XSUAA service and Application Router for secure web application access.
## Detection
This skill applies if any of these patterns are found:
### In web.xml
```xml
<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>
```
### In Java source files
```java
import com.sap.security.um.user.UserProvider;
import com.sap.security.um.user.User;
// OR
request.getUserPrincipal();
request.isUserInRole("Everyone");
```
## Prerequisites
> **Working directory:** This skill must run inside the `-cf-migration` copy of your app, created by `jakarta-java25-migration` or `neo-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:
1. **sdk-replacement** - `Use the sdk-replacement skill`
- Sets up SAP Cloud SDK
- REQUIRED before this skill
## Transformation Steps
### Step 1: Create xs-security.json
Create `xs-security.json` in your project root (or `cf/` folder) - see [assets/xs-security.json](assets/xs-security.json) for template:
```json
{
"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 and `Everyone` with 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 the `role-collections` array. `subaccount-roles-import` reads these names to link deployed role-templates and assign users.
### Step 2: Update web.xml Authentication Method
**Before:**
```xml
<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:**
```xml
<login-config>
<auth-method>XSUAA</auth-method>
</login-config>
```
### Step 3: Add Security Library Dependency
Add to `pom.xml`:
```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-api` artifact is managed by the `cf-tomcat-bom` BOM, so no version is needed.
### Step 4: Create Application Router
Create `approuter/` directory with these files:
#### approuter/package.json
Copy from [assets/package.json](assets/package.json):
```json
{
"name": "approuter",
"dependencies": {
"@sap/approuter": "^16.0.0"
},
"scripts": {
"start": "node node_modules/@sap/approuter/approuter.js"
}
}
```
#### approuter/xs-app.json
See [assets/xs-app.json](assets/xs-app.json) for a complete template:
```json
{
"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 `$XSAPPNAME` placeholder is resolved at runtime to the bound XSUAA service's `xsappname`. Routes without a `scope` field only require authentication (when `authenticationType` is `xsuaa`).
### Step 5: Update User Access Code
#### Neo UserProvider → CF XSUAA Token/TokenClaims mapping
| Neo `UserProvider` / `User` API | CF XSUAA equivalent |
|----------------------------------|---------------------|
| `user.getName()` | `principal.getName()` — returns `user_uuid` (IAS) or `user_name` (XSUAA) |
| `user.getAttribute("email")` | `token.getClaimAsString(TokenClaims.EMAIL)` |
| `user.getAttribute("firstname")` | `token.getClaimAsString(TokenClaims.GIVEN_NAME)` |
| `user.getAttribute("lastname")` | `token.getClaimAsString(TokenClaims.FAMILY_NAME)` |
| `user.getAttribute("displayName")` | `token.getClaimAsString(TokenClaims.SAP_GLOBAL_USER_DISPLAY_NAME)` |
| `user.getId()` (unique ID) | `token.getClaimAsString(TokenClaims.SAP_GLOBAL_USER_ID)` |
| `request.isUserInRole("Admin")` | `request.isUserInRole("Admin")` — unchanged, maps to XSUAA scope |
| Neo tenant / account name | `token.getClaimAsString(TokenClaims.SAP_GLOBAL_ZONE_ID)` |
| `user.getAttribute("logon_name")` | `token.getClaimAsString(TokenClaims.USER_NAME)` |
| XSUAA scopes list | `token.getClaimAsStringList(TokenClaims.XSUAA.SCOPES)` — XSUAA-specific, not available for IAS tokens |
**Before (Neo):**
```java
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();
String email = user.getAttribute("email");
String firstName = user.getAttribute("firstname");
boolean isAdmin = request.isUserInRole("Admin");
}
```
**After (Cloud Foundry — in JAX-RS endpoints or servlets):**
```java
import com.sap.cloud.security.token.SecurityContext;
import com.sap.cloud.security.token.Token;
import com.sap.cloud.security.token.TokenClaims;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.security.Principal;
import java.util.List;
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 1. Basic identity — works with servlet security constraint in web.xml
Principal principal = request.getUserPrincipal();
if (principal == null) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
String userName = principal.getName(); // user_uuid or user_name depending on IdP
// 2. Read claims from the JWT token for richer user attributes
Token token = SecurityContext.getToken();
if (token != null) {
String email = token.getClaimAsString(TokenClaims.EMAIL);
String firstName = token.getClaimAsString(TokenClaims.GIVEN_NAME);
String lastName = token.getClaimAsString(TokenClaims.FAMILY_NAME);
String userId = token.getClaimAsString(TokenClaims.SAP_GLOBAL_USER_ID);
String logonName = token.getClaimAsString(TokenClaims.USER_NAME);
// XSUAA scopes — only available for XSUAA tokens, null for IAS tokens
List<String> scopes = token.getClaimAsStringList(TokenClaims.XSUAA.SCOPES);
// Tenant (zone ID) — identifies the subaccount
String zoneId = token.getClaimAsString(TokenClaims.SAP_GLOBAL_ZONE_ID);
}
// 3. Role / scope check — unchanged from Neo
boolean isAdmin = request.isUserInRole("Admin");
}
```
> **Why `SecurityContext.getToken()`?** The SAP Java Buildpack's `XSSecurityAuthenticator` Catalina valve validates the JWT and stores the parsed `Token` object in a thread-local via `SecurityContext`. This is the correct and recommended way to access the token. `request.getAttribute(Token.class.getName())` does NOT work in this context.
> **`Token` vs `Principal`:** `request.getUserPrincipal().getName()` is sufficient for identifying the user. Only reach for `Token` when you need claims that are not exposed through the standard servlet API — email, given name, family name, zone ID.
> **Null safety:** `token.getClaimAsString(...)` returns `null` if the claim is absent (e.g., the IdP did not include it). Always null-check before use in production code.
### Step 6: Create MTA Descriptor with Approuter
Create or update `mtad.yaml`:
```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/<artifactId>.war # substitute literal artifactId from pom.xml; app serves at /
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.jre.SAPMachineJRE']"
JBP_CONFIG_SAP_MACHINE_JRE: "{ 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:**
View on GitHub