Technology Blog Posts by SAP
cancel
Showing results for 
Search instead for 
Did you mean: 
DenisDuev
Product and Topic Expert
Product and Topic Expert
1,167

Abstract

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:

  • Authentication
  • Authorization

This blog is self-contained.

 

Prerequisites 📋

Same as the previous blog

  • You have enabled the Kyma environment.
  • You have entitlements and quota for the Job Scheduling service.
  • kubectl & kubelogin
  • docker (or podman; replace "docker" with "podman" in the commands below if you choose this option)
  • A registration on Docker Hub (or some other Docker repository)

Here is what the file structure will look like at the end:

kyma-part-2/
├── Dockerfile
├── manifest.yaml
├── package.json
└── server.js

 

Steps 🛠

1. Application 📦

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!

2. Build and publish a Docker image 🐳

The same as the previous blog - I'll keep it minimal, but functional.

2.1 Create a Dockerfile 📄

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"]

2.2 Build a Docker image 🔨

We can use the tag: part2 to keep it separate from the first tutorial.

docker build . -t <your-user>/jobapp:part2

2.3 Create repository on Docker Hub 🗂

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.

  1. Open Docker Hub.
  2. After logging in, go to the Repositories tab.
  3. Choose Create Repository and give it a name jobapp.
  4. Choose Create.

dockerhub-create-repo.png

2.4 Upload the image to a Docker repository ⬆️

  1. Log in to Docker Hub.
docker login -u <your-user>

For podman: podman login docker.io -u <your-user>

  1. Push the image to your Docker registry

Don't forget, we are using "part2" here.

docker push <your-user>/jobapp:part2

For podman: podman push jobapp:part2 <your-user>/jobapp:part2

docker-push.png

3. Apply to Kyma cluster ☁️

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.

3.1 Download and set up Kubeconfig 📥

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.

kyma-cockpit.png

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%

3.3 Create manifest.yaml 📜

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:

  • The namespace is created in the YAML as part of the definition.
  • The ServiceInstance and ServiceBinding for xsuaa are defined.
  • xsuaa requires some additional configuration which is passed as part of the instance definition.
  • The xsuaa binding's secret is attached as volume to the Deployment.

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

3.4 Apply changes to the cluster 🚀

kubectl apply -f manifest.yaml

Note: You may need to wait some minutes until all resources are successfully created (reconciled).

kubectl-appy.png

4. Validate the application

4.1 Calling your application

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).

test-app.png

4.2 Checking your resources in Kyma

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.

service-binding.png

Or, you can see how the deployment looks, where the Pod has the two secrets for the two service instances we've created.

deployment.png

5. Testing the Job Scheduling Service 🕒

5.1 Permissions for the Job Scheduling service dashboard 🔑

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.

  1. In your subaccount, from the Security menu, choose Role Collections.
  2. Choose Create.
  3. In the dialog, add a name SAP Job Scheduling Service Admin and description Assign it to access Job Scheduling Service Dashboard and manage jobs, then choose Create.
  4. Choose the newly created role collection SAP_Job_Scheduling_Service_Admin.
  5. Choose Edit.
  6. In the Roles tab, find the SAP_Job_Scheduling_Service_Admin Role Name, mark it in the list, and choose Add.
  7. Choose Save on the role collection,
  8. From the Security menu, go to Users.
  9. Find your user, select it, then choose Assign Role Collection.
  10. From the list, find SAP_Job_Scheduling_Service_Admin, mark it, and then choose Assign Role Collection.

kyma-jobscheduling-roles.gif

5.2 Link to the dashboard 🔗

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:

dashboard.png

5.3 Create job 📝

  1. From the Job Scheduling service dashboard, choose Jobs in the left-hand menu.
  2. Then choose Create Job.
  3. In the creation dialog, specify the required information:
    • name - this is the technical name, for example: jobapp_get_runjob
    • Action - this is the endpoint of your service that we want to call (from step 4. Validate the application) https://jobapp-part2-<your-suffix>.<kyma-apps-domain>/runjob
    • HTTP Method - in our case GET is correct
  4. Choose Save and the dialog is closed.
  5. Then choose the Name of the newly created job. The Overview page opens.
  6. From the left-hand menu, choose Schedules. Then choose Create Schedule.
  7. For "value", enter now and choose Save. This means that the scheduler will be executed only one time - now.
  8. Choose the Description of the newly created schedule. The Overview opens.
  9. From the left-hand menu, choose Run Logs to see the executions for this schedule.
  10. If you choose Runlog ID, you can see more details about the specific run (execution).

create-job-kyma.gif


🎉 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.

job-execution.png

6. Cleanup

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 🔝.

kubectl-delete.png