You may have seen the first blog in this Job Scheduling service in Kyma series: SAP Job Scheduling Service in Kyma: Simple Use Case.
In this blog, we will build a complex solution using XSUAA to include:
This blog is self-contained.
Same as the previous blog
Here is what the file structure will look like at the end:
kyma-part-2/ ├── Dockerfile ├── manifest.yaml ├── package.json └── server.js
This time the package.json will contain 3 additional dependencies:
{ "main": "server.js", "dependencies": { "express": "4.20.0", "passport": "0.7.0", "@sap/xsenv": "5.3.0", "@sap/xssec": "4.2.6" } }
Let's now check the server.js code.
First, we summon our trusty sidekicks: Express, Passport, and a few SAP-specific packages. We then retrieve our secret XSUAA credentials (🤫 shh, don’t tell anyone!) and create a XssecPassportStrategy to handle our authentication needs.
Next, we set up an Express server and introduce a middleware function, jwtLogger, which is like a security guard 🕵️♂️ that checks and logs the details of every JWT token that comes its way. It decodes the token, logs important info like scopes, client IDs, and usernames, and then lets the request proceed.
We then tell Passport to use our XssecPassportStrategy and set it up to authenticate requests without creating a session.
Finally, we create an endpoint /runjob that checks if the incoming request has the right scope. If it does, it runs a super important job and sends a success message. If not, it sends a simple “Unauthorized” message 🚫.
const express = require("express"); const passport = require("passport"); const xsenv = require("@sap/xsenv"); const { XssecPassportStrategy, XsuaaService } = require("@sap/xssec"); // Retrieve the XSUAA service credentials from the environment const xsuaaService = xsenv.getServices({ myXsuaa: { tag: "xsuaa" } }); const xsuaaCredentials = xsuaaService.myXsuaa; const authService = new XsuaaService(xsuaaCredentials); // Use the XssecPassportStrategy with Passport.js for authentication passport.use(new XssecPassportStrategy(authService)); // configure express server with authentication middleware const app = express(); // Middleware to read JWT sent by JobScheduler /** * Middleware function that logs details of a JWT token from the request's authorization header send by Job Scheduling service. * It extracts the JWT token, decodes its payload, and logs specific fields such as scopes, client_id, and user_name. * This function helps in monitoring and debugging by providing insights into the JWT token being processed. * Finally, it calls the next middleware in the stack. */ function jwtLogger(req, res, next) { console.log("===> [MIDDLEWARE] decoding auth header"); // Extract the authorization header const authHeader = req.headers.authorization; if (authHeader) { // Remove the "Bearer " prefix to get the JWT token const theJwtToken = authHeader.substring(7); if (theJwtToken) { console.log(`===> [MIDDLEWARE] the received JWT token: ${theJwtToken}`); // Extract the payload part of the JWT token const jwtBase64Encoded = theJwtToken.split(".")[1]; if (jwtBase64Encoded) { // Decode the Base64-encoded payload const jwtDecoded = Buffer.from(jwtBase64Encoded, "base64").toString("ascii"); const jwtDecodedJson = JSON.parse(jwtDecoded); console.log(`===> [MIDDLEWARE]: JWT: scopes: ${jwtDecodedJson.scope}`); console.log(`===> [MIDDLEWARE]: JWT: client_id: ${jwtDecodedJson.client_id}`); console.log(`===> [MIDDLEWARE]: JWT: user: ${jwtDecodedJson.user_name}`); } } } next(); } // Use the jwtLogger middleware to log details of the JWT token from the request's authorization header app.use(jwtLogger); app.use(passport.initialize()); // Use Passport.js to authenticate requests using the JWT strategy, without creating a session app.use(passport.authenticate("JWT", { session: false })); app.get("/runjob", function (req, res) { const MY_SCOPE = xsuaaCredentials.xsappname + ".scopeformyapp"; // Check if the JWT token contains the required scope if (req.authInfo.checkScope(MY_SCOPE)) { console.log("[APP JOB LOG] Job is running . . ."); console.log("[APP JOB LOG] Doing some very important work . . ."); console.log("[APP JOB LOG] Job is finished."); // Send a response to Job Scheduling service indicating the job has finished successfully res.send( "Finished job. The endpoint was properly called, the required scope has been found in JWT token." ); } else { return res.status(403).json({ error: "Forbidden", message: "The endpoint was called by user who does not have the required scope: <scopeformyapp> ", }); } }); const port = process.env.PORT || 80; app.listen(port, function () { console.log("Listening..."); });
Great job!
The same as the previous blog - I'll keep it minimal, but functional.
FROM --platform=linux/amd64 node:20 WORKDIR /usr/src/app COPY package*.json ./ RUN npm install ENV PORT 80 COPY . . EXPOSE 80 CMD ["npm","run","start"]
We can use the tag: part2 to keep it separate from the first tutorial.
docker build . -t <your-user>/jobapp:part2
You can skip this step if you have the repository from the first tutorial.
These instructions are for Docker Hub, but you can use any other Docker repository you want.
We need this repository to upload the image we've built in the previous step.
docker login -u <your-user>
For podman: podman login docker.io -u <your-user>
Don't forget, we are using "part2" here.
docker push <your-user>/jobapp:part2
For podman: podman push jobapp:part2 <your-user>/jobapp:part2
Again, the general steps are the same - the difference is in the manifest.yaml.
In order to work with Kyma (which is in fact, a Kubernetes cluster with additional custom resource definitions), we need a Kubeconfig to authenticate and the kubectl CLI.
1. Open the SAP BTP cockpit.
2. Under your subaccount, go to Overview, and find the section Kyma Environment.
3. Open the KubeconfigURL link. This will download the Kubeconfig file on your local PC.
4. Then open your terminal (Mac, Linux):
export KUBECONFIG=<location of the downloaded file>For windows use: Powershell: $env:KUBECONFIG = "…\kubeconfig.yaml", to verify echo $env:KUBECONFIG CMD: set KUBECONFIG=…\kubeconfig.yaml, to verify echo %KUBECONFIG%
This YAML file contains all of the configuration for the Kyma cluster to deploy our application, expose an endpoint, create a service instance and bindings, and mount them to the deployment of our application.
What is different from the first blog:
Note that you need to fill:
image: <your-username>/jobapp:part2 (...) host: jobapp-part2-<your-suffix>.<kyma-apps-domain>
You can find help as comments in the manifest.yaml.
# creating the namespace where all resources will be defined # it's later used for every resource apiVersion: v1 kind: Namespace metadata: name: jobapp-part2 --- # defining the service instance and binding (same as part 1) apiVersion: services.cloud.sap.com/v1 kind: ServiceInstance metadata: name: jobscheduler-instance labels: app.kubernetes.io/name: jobscheduler-instance annotations: {} namespace: jobapp-part2 spec: serviceOfferingName: jobscheduler servicePlanName: standard --- apiVersion: services.cloud.sap.com/v1 kind: ServiceBinding metadata: name: jobscheduler-binding labels: app.kubernetes.io/name: jobscheduler-binding annotations: {} namespace: jobapp-part2 spec: serviceInstanceName: jobscheduler-instance --- # 🆕 defining the second service instance - XSUAA apiVersion: services.cloud.sap.com/v1 kind: ServiceInstance metadata: name: xsuaa-instance labels: app.kubernetes.io/name: xsuaa-instance annotations: {} namespace: jobapp-part2 spec: # the values serviceOfferingName & servicePlanName supplied here are the ones from the Service Marketplace in the cockpit ## serviceOfferingName is the "Technical name serviceOfferingName: xsuaa servicePlanName: broker parameters: # here we define what is usually done inside 'xs-security.json' but this time in YAML format xsappname: jobapp-part2 scopes: - description: Users of my great app need this special role grant-as-authority-to-apps: # ❗ 'jobscheduler-instance' is the name you've defined for your instance - make sure they match - $XSSERVICENAME(jobscheduler-instance) name: $XSAPPNAME.scopeformyapp --- # 🆕 defining the Service Binding for xsuaa apiVersion: services.cloud.sap.com/v1 kind: ServiceBinding metadata: # this will be the name of your service instance name: xsuaa-binding labels: app.kubernetes.io/name: xsuaa-binding annotations: {} namespace: jobapp-part2 spec: # specify the name of the service-instance serviceInstanceName: xsuaa-instance --- apiVersion: v1 kind: Service metadata: namespace: jobapp-part2 name: jobapp labels: run: jobapp spec: type: ClusterIP ports: - port: 80 targetPort: 80 protocol: TCP name: http selector: app: jobapp --- apiVersion: apps/v1 kind: Deployment metadata: namespace: jobapp-part2 name: jobapp labels: app: jobapp version: nodejs spec: replicas: 1 selector: matchLabels: app: jobapp version: nodejs template: metadata: labels: app: jobapp version: nodejs spec: containers: - name: jobapp # 👉 replace <your-username> with your Docker Hub image namespace/user 👈 image: <your-username>/jobapp:part2 imagePullPolicy: Always ports: - containerPort: 80 env: - name: SERVICE_BINDING_ROOT value: /bindings volumeMounts: - mountPath: /bindings/jobscheduler-instance name: jobscheduler-volume readOnly: true - mountPath: /bindings/xsuaa-instance # 🆕 this is the name of the volume we've defined below that points to the Binding's secret name: xsuaa-volume readOnly: true volumes: - name: jobscheduler-volume secret: defaultMode: 420 secretName: jobscheduler-binding # 🆕 when defining a ServiceBinding (above), it creates a Secret with the same name # this secret is then bound as a volume - name: xsuaa-volume secret: defaultMode: 420 secretName: xsuaa-binding --- apiVersion: gateway.kyma-project.io/v1beta1 kind: APIRule metadata: namespace: jobapp-part2 name: jobapp labels: app.kubernetes.io/name: jobapp spec: gateway: kyma-gateway.kyma-system.svc.cluster.local rules: - accessStrategies: - handler: allow config: {} methods: - GET - POST - PUT - DELETE path: /.* # 👉 host is the address that your app will be accessible over 👈 # it you can get <kyma-apps-domain> from the APIServerURL in the cockpit by removing the https://api or # by executing: # kubectl config view --minify --output 'jsonpath={.clusters[0].cluster.server}' | awk -F'api.' '{print $2}' host: jobapp-part2-<your-suffix>.<kyma-apps-domain> service: name: jobapp port: 80
kubectl apply -f manifest.yaml
Note: You may need to wait some minutes until all resources are successfully created (reconciled).
Now that your application is deployed, it's time to test it. As we have Authorization in place you should get unauthorized. But the Job Scheduling service will be able to successfully call it. If you are curious how you can call it from your machine, check the original blog for CloudFoundry - Simple use case with authentication and authorization.
Open your endpoint in the browser (for example, https://jobapp-part2-<your-suffix>.<kyma-apps-domain>/runjob).
You should have specified this URL in the manifest.yaml (host).
If you are curious, you can check all of the resources defined in the manifest.yaml in the Kyma UI or using kubectl.
For example, the ServiceBindings create a Secret that contains the configurations and is also bound to the ServiceInstance.
Or, you can see how the deployment looks, where the Pod has the two secrets for the two service instances we've created.
Same as in the previous blog, skip if you've already done it.
To view the dashboard, your user has to have the SAP_Job_Scheduling_Service_Admin role assigned from the SAP BTP cockpit.
To find the dashboard URL:
1. Go to Instances and Subscriptions in the SAP BTP cockpit.
2. Choose the "jobscheduler" service instance.
3. Choose View Dashboard:
🎉 That's it! 🎉
🥳 You have done it again - you have successfully called your xsuaa-protected Node.js app, deployed on Kyma using the SAP Job Scheduling service and understood how using xsuaa you can make you application secure.
After you are ready, you can delete all of your resources using:
kubectl delete -f manifest.yaml
so that you are ready for our 🔝 next tutorial 🔝.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
| User | Count |
|---|---|
| 47 | |
| 22 | |
| 18 | |
| 16 | |
| 15 | |
| 14 | |
| 14 | |
| 14 | |
| 13 | |
| 13 |