Quicklinks:
Intro Blog
Sample Code
{
"dependencies": {
"@sap/xssec": "^3.0.10",
"passport": "^0.4.0",
"express": "^4.16.2",
"node-fetch": "2.6.2"
}
}
app.get('/app', async function(req, res){
app.get('/app', async function(req, res){
const hostname = req.hostname
const subdomain = hostname.substring(0,hostname.lastIndexOf('-'))
const userJwtToken = await fetchUserTokenWithUserPassword(subdomain)
async function fetchUserTokenWithUserPassword(subdomain) {
const oauthEndpoint = `https://${subdomain}.${UAA_CREDENTIALS.uaadomain}/oauth/token?grant_type=password&username=${USER}&password=${PWD}`
const result = await fetch(oauthEndpoint, {
headers: {
Authorization: "Basic " + Buffer.from(UAA_CREDENTIALS.clientid + ':' + UAA_CREDENTIALS.clientsecret).toString("base64")
const user = decode(userJwtToken).given_name
function composeWebsite(userJwtToken, user, hostname){
const src = `<script src="${path.basename(__filename)}"></script>`
const ajx = `<script src = https://code.jquery.com/jquery-3.6.0.min.js></script>`
const href = `javascript:onClick('${hostname}', '${userJwtToken}')`
return `${src}${ajx}<h1>Homepage</h1><p>Hello ${user}, <a href="${href}">click</a> to create job.</p>`
}
const href = `javascript:onClick('${hostname}', '${userJwtToken}')`
const src = `<script src="${path.basename(__filename)}"></script>`
app.use(express.static(__dirname))
const ajx = `<script src = https://code.jquery.com/jquery-3.6.0.min.js></script>`
return `${src}${ajx}<h1>Homepage</h1><p>Hello ${user}, <a href="${href}">click</a> to create job.</p>`
async function onClick(hostname, jwtToken){
$.ajax({
url: `https://${hostname}/createjob`,
headers: { "Authorization": "bearer " + jwtToken },
success: function(data){
const p = document.createElement("P")
p.appendChild(document.createTextNode("-> Received response: " + data))
document.body.appendChild(p)
app.get('/createjob', passport.authenticate('JWT', {session: false}), async function(req, res) {
const userJwtToken = req.headers.authorization.substring(7)
const exchangedToken = await doTokenExchange(userJwtToken)
const result = await createJob(exchangedToken)
res.send(`Result of job creation: ${JSON.stringify(result)}`)
});
const UAA_CREDENTIALS = VCAP_SERVICES.xsuaa[0].credentials
passport.use('JWT', new JWTStrategy(UAA_CREDENTIALS))
passport.authenticate('JWT'
const JOBSCH_CREDENTIALS = VCAP_SERVICES.jobscheduler[0].credentials
const JOB_UAA = JOBSCH_CREDENTIALS.uaa
const userJwtToken = req.headers.authorization.substring(7)
async function doTokenExchange (bearerToken){
const jwtDecoded = decode(bearerToken)
const consumerSubdomain = jwtDecoded.ext_attr.zdn
return new Promise ((resolve, reject) => {
xssec.requests.requestUserToken(bearerToken, JOB_UAA, null, null, consumerSubdomain, null, (error, token)=>{
resolve(token)
const jwtDecoded = decode(bearerToken)
const consumerSubdomain = jwtDecoded.ext_attr.zdn
xssec.requests.requestUserToken(bearerToken, JOB_UAA, null, null, consumerSubdomain, null, (error, token)=>{
resolve(token)
grantType = 'urn:ietf:params:oauth:grant-type:jwt-bearer'
body = `grant_type=${grantType}&response_type=token&assertion=${bearerToken}`
async function createJob(jwtToken) {
const jwtDecoded = decode(jwtToken)
const body = {
name: `MuteteJob_${new Date().getMilliseconds()}`,
action: `https://${jwtDecoded.ext_attr.zdn}-mutete.cfapps.eu10.hana.ondemand.com/action`,
active: true,
httpMethod: 'GET',
schedules: [{
time: 'now',
active: 'true'
}]
}
const response = await fetch(`${JOBSCH_CREDENTIALS.url}/scheduler/jobs`, {
method: 'POST',
body: JSON.stringify(body),
headers: {
Authorization: 'Bearer ' + jwtToken,
'Content-Type': 'application/json'
},
})
const responseJson = await response.json();
return {jobName: responseJson.name,
jobID: responseJson._id,
createdBy: jwtDecoded.user_name,
}
}
async function fetchTokenForJobWithExchangeClassic(bearerToken) {
const consumerSubdomain = decodeConsumerSubdomain(bearerToken)
const uaadomain = JOB_UAA.uaadomain
const oauthEndpoint = `${consumerSubdomain}.${uaadomain}`
return new Promise ((resolve, reject) => {
const options = {
host: oauthEndpoint,
path: '/oauth/token',
method: 'POST',
headers: {
Authorization: "Basic " + Buffer.from(JOB_UAA.clientid + ':' + JOB_UAA.clientsecret).toString("base64"),
Accept: 'application/json',
'Content-Type': 'application/x-www-form-urlencoded'
}
}
const granttype = 'urn:ietf:params:oauth:grant-type:jwt-bearer'
// const data = `client_id=${JOB_CREDENTIALS.clientid}&client_secret=${JOB_CREDENTIALS.clientsecret}&grant_type=${granttype}&response_type=token&assertion=${bearerToken}`
// const data = `client_id=${JOB_CREDENTIALS.clientid}&grant_type=${granttype}&token_format=jwt&response_type=token&assertion=${bearerToken}`
// const data = `client_id=${JOB_CREDENTIALS.clientid}&grant_type=${granttype}&token_format=opaque&response_type=token&assertion=${bearerToken}`
// const data = `client_id=${JOB_CREDENTIALS.clientid}&grant_type=${granttype}&response_type=token&assertion=${bearerToken}`
// const data = `grant_type=${granttype}&response_type=token+id_token&assertion=${bearerToken}`
const data = `grant_type=${granttype}&response_type=token&assertion=${bearerToken}`
const req = https.request(options, (res) => {
res.setEncoding('utf8')
let response = ''
res.on('data', chunk => {
response += chunk
})
res.on('end', () => {
try {
const responseAsJson = JSON.parse(response)
resolve(responseAsJson.access_token)
} catch (error) {
return reject(new Error('Error while fetching JWT token'))
}
})
})
req.on('error', (error) => {
return reject({error: error})
});
req.write(data)
req.end()
})
}
{
"xsappname": "mutetexsappname",
"tenant-mode": "shared"
}
{
"appId": "mutetexsappname!t17916",
"appName": "muteteAppNameForSaasReg",
"appUrls": {
"getDependencies" : "https://mutete.cfapps.eu10.hana.ondemand.com/handleDependencies",
"onSubscription" : "https://mutete.cfapps.eu10.hana.ondemand.com/handleSubscription/{tenantId}"
},
"displayName": "MuTeTe with Jobscheduler"
}
---
applications:
- name: mutete
routes:
- route: mutete.cfapps.eu10.hana.ondemand.com
- route: jobschedulertest-mutete.cfapps.eu10.hana.ondemand.com
memory: 128M
services:
- muteteSaasreg
- muteteXsuaa
- myJobschedulerInstance
{
"dependencies": {
"@sap/xssec": "^3.0.10",
"passport": "^0.4.0",
"express": "^4.16.2",
"node-fetch": "2.6.2"
}
}
const USER = 'my.user@mymail.com'
const PWD = 'mypwd'
const VCAP_SERVICES = JSON.parse(process.env.VCAP_SERVICES)
const UAA_CREDENTIALS = VCAP_SERVICES.xsuaa[0].credentials
const JOBSCH_CREDENTIALS = VCAP_SERVICES.jobscheduler[0].credentials
const JOB_UAA = JOBSCH_CREDENTIALS.uaa
const path = require('path') // required for dynamically retrieving current js file
const fetch = require('node-fetch')
const express = require('express')
const app = express()
app.use(express.json())
const passport = require('passport')
const xssec = require('@sap/xssec')
const JWTStrategy = xssec.JWTStrategy
passport.use('JWT', new JWTStrategy(UAA_CREDENTIALS))
app.use(passport.initialize())
app.use(express.static(__dirname)) // Express Middleware for serving static files, required for our ui-simulation (load our js file into webserver)
/* App server */
app.listen(process.env.PORT, () => {});
app.get('/app', async function(req, res){
const hostname = req.hostname
const subdomain = hostname.substring(0,hostname.lastIndexOf('-'))
//simulation of user-login screen (tenant-specific)
const userJwtToken = await fetchUserTokenWithUserPassword(subdomain)
const user = decode(userJwtToken).given_name
const website = composeWebsite(userJwtToken, user, hostname)
res.send(website)
});
app.get('/createjob', passport.authenticate('JWT', {session: false}), async function(req, res) {
const userJwtToken = req.headers.authorization.substring(7) //remove the "bearer_"
// token exchange: send user token and get Jobscheduler token (for calling REST API)
const exchangedToken = await doTokenExchange(userJwtToken)
const result = await createJob(exchangedToken)
res.send(`Result of job creation: ${JSON.stringify(result)}`)
});
app.get('/action', function(req, res){
res.send(`"/action" endpoint invoked by jobscheduler. Customer status updated for ${req.hostname}.`)
});
/* Multi Tenancy callbacks */
app.get('/handleDependencies', (req, res) => {
const dependencies = [{'xsappname': JOBSCH_CREDENTIALS.uaa.xsappname }]
res.status(200).json(dependencies);
});
app.put('/handleSubscription/:myConsumer', (req, res) => {
const appHost = req.hostname
const subDomain = req.body.subscribedSubdomain
res.status(200).send(`https://${subDomain}-${appHost}/app`)
});
app.delete('/handleSubscription/:myConsumer', (req, res) => {
res.status(200).end('unsubscribed')
});
/* THE WEBSITE */
function composeWebsite(userJwtToken, user, hostname){
const src = `<script src="${path.basename(__filename)}"></script>`
const ajx = `<script src = "https://code.jquery.com/jquery-3.6.0.min.js"></script>`//"https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"
const href = `javascript:onClick('${hostname}', '${userJwtToken}')`
return `${src}${ajx}<h1>Homepage</h1><p>Hello ${user}, <a href="${href}">click</a> to create job.</p>`
}
// when click on hyperlink we manually fire REST request to call our createJob endpoint
async function onClick(hostname, jwtToken){
$.ajax({
url: `https://${hostname}/createjob`,
headers: { "Authorization": "bearer " + jwtToken },
success: function(data){
const p = document.createElement("P")
p.appendChild(document.createTextNode("-> Received response: " + data))
document.body.appendChild(p)
}
})
}
/* HELPER */
//simulating a user-login-screen. instead of login-form, fetch token with hard-coded credentials
async function fetchUserTokenWithUserPassword(subdomain) {
const oauthEndpoint = `https://${subdomain}.${UAA_CREDENTIALS.uaadomain}/oauth/token?grant_type=password&username=${USER}&password=${PWD}`
const result = await fetch(oauthEndpoint, {
headers: {
Authorization: "Basic " + Buffer.from(UAA_CREDENTIALS.clientid + ':' + UAA_CREDENTIALS.clientsecret).toString("base64")
}
});
const responseAsJson = await result.json();
return responseAsJson.access_token;
}
async function doTokenExchange (bearerToken){
const jwtDecoded = decode(bearerToken)
const consumerSubdomain = jwtDecoded.ext_attr.zdn
return new Promise ((resolve, reject) => {
xssec.requests.requestUserToken(bearerToken, JOB_UAA, null, null, consumerSubdomain, null, (error, token)=>{
resolve(token)
})
})
}
async function createJob(jwtToken) {
const jwtDecoded = decode(jwtToken)
const body = {
name: `MuteteJob_${new Date().getMilliseconds()}`,
action: `https://${jwtDecoded.ext_attr.zdn}-mutete.cfapps.eu10.hana.ondemand.com/action`,
active: true,
httpMethod: 'GET',
schedules: [{
time: 'now',
active: 'true'
}]
}
const response = await fetch(`${JOBSCH_CREDENTIALS.url}/scheduler/jobs`, {
method: 'POST',
body: JSON.stringify(body),
headers: {
Authorization: 'Bearer ' + jwtToken,
'Content-Type': 'application/json'
},
})
const responseJson = await response.json();
return {jobName: responseJson.name,
jobID: responseJson._id,
createdBy: jwtDecoded.user_name,
}
}
function decode(jwtToken){
const jwtBase64Encoded = jwtToken.split('.')[1];
const jwtDecodedAsString = Buffer.from(jwtBase64Encoded, 'base64').toString('ascii');
return JSON.parse(jwtDecodedAsString);
}
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
User | Count |
---|---|
11 | |
10 | |
10 | |
10 | |
8 | |
7 | |
7 | |
7 | |
7 | |
6 |