How to automate the login on SAP IdP in an end-to-end test - sap-cloud-sdk

Our backend API's auth method's been replaced to OAuth 2.0.
Now we would like to write end2end auth testing with different business users.
My idea is to write testing code in the BTP, which will call the backend OAuth enabled SAP service.
Just followed the e2e test tutorial using nightwatch and cucumber.
But now the logon page has been changed to the SAP IDP logon page.
Do you know how to automate the login&logout for this idp logon page?
Thanks a lot!
idp logon page
I could not find the element name of username and the password in the idp logon page.

In the JS SDK We use puppeteer to fetch a token programatically. In the end it also provides the user name and password to the IdP. Here is a sample:
import https from 'https';
import { createLogger } from '#sap-cloud-sdk/util';
import { Service, parseSubdomain } from '#sap-cloud-sdk/connectivity/internal';
import puppeteer from 'puppeteer';
import axios from 'axios';
import { UserNamePassword } from './test-parameters';
const logger = createLogger('e2e-util');
export interface UserTokens {
access_token: string;
refresh_token: string;
}
export interface GetJwtOption {
redirectUrl: string;
route: string;
xsuaaService: Service;
subdomain: string;
userAndPwd: UserNamePassword;
}
export async function getJwt(options: GetJwtOption): Promise<UserTokens> {
const { redirectUrl, route, userAndPwd, xsuaaService, subdomain } = options;
const xsuaaCode = await getAuthorizationCode(redirectUrl, route, userAndPwd);
return getJwtFromCode(xsuaaCode, redirectUrl, xsuaaService, subdomain);
}
export async function getJwtFromCode(
xsuaaCode: string,
redirectUri: string,
xsuaaService: Service,
subdomain: string
): Promise<UserTokens> {
let httpsAgent: https.Agent;
const params = new URLSearchParams();
params.append('redirect_uri', `${redirectUri}/login/callback`);
params.append('code', xsuaaCode);
params.append('grant_type', 'authorization_code');
params.append('client_id', xsuaaService.credentials.clientid);
if (xsuaaService.credentials.clientsecret) {
params.append('client_secret', xsuaaService.credentials.clientsecret);
httpsAgent = new https.Agent();
} else {
httpsAgent = new https.Agent({
cert: xsuaaService.credentials.certificate,
key: xsuaaService.credentials.key
});
}
const url = xsuaaService.credentials.clientsecret
? xsuaaService.credentials.url
: xsuaaService.credentials.certurl;
const subdomainProvider = parseSubdomain(url);
const urlReplacedSubdomain = url.replace(subdomainProvider, subdomain);
const response = await axios.post(
`${urlReplacedSubdomain}/oauth/token`,
params,
{
httpsAgent
}
);
if (!response.data.access_token) {
throw new Error('Failed to get the JWT');
}
logger.info(`Obtained JWT for ${redirectUri}.`);
return {
access_token: response.data.access_token,
refresh_token: response.data.refresh_token
};
}
async function getAuthorizationCode(
url: string,
route: string,
userAndPwd: UserNamePassword
): Promise<string> {
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox']
});
const page = await browser.newPage();
await page.setRequestInterception(true);
// Catch all failed requests like 4xx..5xx status codes
page.on('requestfailed', request => {
if (!request.failure()!.errorText.includes('ERR_ABORTED')) {
logger.error(
`url: ${request.url()}, errText: ${
request.failure()?.errorText
}, method: ${request.method()}`
);
}
});
// Catch console log errors
page.on('pageerror', err => {
logger.error(`Page error: ${err.toString()}`);
});
page.on('request', request => {
if (request.url().includes('/login/callback?code=')) {
request.abort('aborted');
} else {
request.continue();
}
});
try {
await Promise.all([
await page.goto(`${url}/${route}`),
await page.waitForSelector('#j_username', {
visible: true,
timeout: 5000
})
]);
} catch (err) {
throw new Error(
`The #j_username did not show up on URL ${url}/${route} - perhaps you have the identityProvider in the xs-security.json of your approuter?`
);
}
await page.click('#j_username');
await page.keyboard.type(userAndPwd.username);
const passwordSelect = await page
.waitForSelector('#j_password', { visible: true, timeout: 1000 })
.catch(() => null);
// For ldap IdP one step in between with navigation to second page
if (!passwordSelect) {
await Promise.all([
page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
page.click('button[type="submit"]')
]);
}
await page.click('#j_password');
await page.keyboard.type(userAndPwd.password);
const [authCodeResponse] = await Promise.all([
page.waitForResponse(response =>
response.url().includes('oauth/authorize?')
),
page.click('button[type="submit"]')
]);
await browser.close();
const parsedLocation = new URL(authCodeResponse.headers().location);
if (!parsedLocation.searchParams.get('code')) {
throw new Error('Final location redirect did not contain a code');
}
return parsedLocation.searchParams.get('code');
}
Best
Frank

Related

Cypress automation test continuously generating authentication token while doing authentication test in SharePoint Sites

All the test is passing successfully but still the cypress test is generating token continuously.
cypress.config.js
const { defineConfig } = require("cypress");
const spauth = require("node-sp-auth");
let getLoginTest = async () => {
const username = "****#****.com";
const password = "******";
const pageUrl = "https://*****.sharepoint.com"
// Connect to SharePoint
const data = await spauth.getAuth(pageUrl, {
username: username,
password: password
});
return data;
};
module.exports = defineConfig({
e2e: {
setupNodeEvents(on, config) {
// implement node event listeners here
on('task', {
// deconstruct the individual properties
async getLogin() {
try {
const res = await getLoginTest();
return res;
} catch (error) {
return error;
}
}
});
},
},
});
Below I have mentioned the tests case which I have created."Testspec.cy.js"
// <reference types="cypress" />
require('cypress-xpath')
describe('SharePoint Authentication', () => {
beforeEach(() => {
cy.task("getLogin").then(token => {
cy.visit({
method: "GET",
url: "https://*****.sharepoint.com/SitePages/Home.aspx",
headers: token.headers
});
});
});
it('SharePoint Authentication Test', () => {
cy.xpath('//*[contains(text(),"Customer Dashboard")]').should('be.visible');
cy.get('.alphabet > :nth-child(3)').click();
cy.contains('Leader').click();
});
});
Test Screen
Here is the screenshot of the cypress test

Load confirmation URL in Embedded app itself in Shopify

I have an embedded app in shopify which is an paid app ,Once user approves the billing ,i want the app to show the confirmation url in the embedded app itself instead it loads externally.
getsubscriptionurl.js
export const getSubscriptionUrl = async (ctx, shop) => {
const { client } = ctx;
console.log(`process.env.HOST - ${process.env.HOST}`);
console.log(`shop - ${shop}`);
console.log(`${process.env.HOST}/?shop=${shop}`);
const confirmationUrl = await client
.mutate({
mutation: RECURRING_CREATE(),
variables: {
returnUrl: `www.abc.com`,
}
})
.then(response => response.data.appSubscriptionCreate.confirmationUrl);
console.log("me "+ confirmationUrl);
return ctx.redirect(confirmationUrl);
};
server.js
app.prepare().then(async () => {
const server = new Koa();
const router = new Router();
server.keys = [Shopify.Context.API_SECRET_KEY];
server.use(
createShopifyAuth({
async afterAuth(ctx) {
// Access token and shop available in ctx.state.shopify
const { shop, accessToken, scope } = ctx.state.shopify;
const host = ctx.query.host;
ACTIVE_SHOPIFY_SHOPS[shop] = {scope:scope,accessToken:accessToken};
const response = await Shopify.Webhooks.Registry.register({
shop,
accessToken,
path: "/webhooks",
topic: "APP_UNINSTALLED",
webhookHandler: async (topic, shop, body) =>
delete ACTIVE_SHOPIFY_SHOPS[shop],
});
if (!response.success) {
console.log(
`Failed to register APP_UNINSTALLED webhook: ${response.result}`
);
}
// Redirect to app with shop parameter upon auth
// ctx.redirect(`/?shop=${shop}&host=${host}`);
server.context.client = await handlers.createClient(shop, accessToken);
await handlers.getSubscriptionUrl(ctx, shop);
},
})
);
You can't basically show the confirmation URL in your app, Shopify won't trust app developers to take sensitive info like payment details, so must open the confirmation URL into a new tab, where the merchant is viewing a Shopify payment page(made by shopify) that contains the payment details to be entered and on confirm the page will redirect the merchant to the return URL as you specified before.
For testing purposes
you can send a test param within the query to allow you to test without entering any payment details
const CREATE_SUB_MUTATION_RECURRING_ONLY = gql`
mutation RecurringSubscription(
$returnUrl: URL!
$test: Boolean!
$planName: String!
$amount: Decimal!
) {
appSubscriptionCreate(
test: $test
name: $planName
returnUrl: $returnUrl
lineItems: [
{
plan: {
appRecurringPricingDetails: {
price: { amount: $amount, currencyCode: USD }
interval: EVERY_30_DAYS
}
}
}
]
) {
userErrors {
field
message
}
confirmationUrl
appSubscription {
id,
currentPeriodEnd
}
}
}
`;
Now to test just pass true to test
result = await graphQlClient?.mutate({
mutation: CREATE_SUB_MUTATION_RECURRING_ONLY,
variables: {
returnUrl,
test,
planName: PLANS_DATA[planName].planName,
amount: PLANS_DATA[planName].price,
},
});

NUXTJS SSR: I have problems with the nuxtServerInit, the req.headers.cookie of the nuxtServerInit is undefined and the cookie is already in my browser

I am new to NuxtJS and have a problem, I am trying to get the token of the user who logged in from the website, the token is stored in the cookie, but, when I start or reload the website page (made with nuxt ssr (universal )), nuxtServerInit should and does start, but req.headers.cookie says it is undefined, so the data cannot be loaded into the store. If I reload the page, the cookie is still in the browser and everything is perfect, the problem is that the req.headers.cookie is: undefined, why? Ahhh, and in development it works perfectly, but in production it doesn't work (in nuxtServerInit the req.headers.cookie is not defined)
I am using Firebase Hosting, Cloud Features, Authentication, Cloud Firestore.
Here is the code:
// store/index.js
import { getUserFromCookie, getUserFromSession } from '../helpers/index.js'
export const actions = {
async nuxtServerInit({ app, dispatch }, { req, beforeNuxtRender }) {
console.log('req.headers.cookie: ' + req.headers.cookie)
console.log('req.session: ', req.session)
if (process.server) {
const user = getUserFromCookie(req)
console.log('process.server: ' + process.server)
console.log('user: ', user)
if (user) {
await dispatch('modules/user/setUSER', {
name: !!user.name ? user.name : '',
email: user.email,
avatar: !!user.picture ? user.picture : '',
uid: user.user_id
})
await dispatch('modules/user/saveUID', user.user_id)
} else {
await dispatch('modules/user/setUSER', null)
await dispatch('modules/user/saveUID', null)
}
}
}
}
// helpers/index.js
import jwtDecode from 'jwt-decode'
var cookieparser = require('cookieparser')
export function getUserFromCookie(req) {
if(req.headers.cookie){
const parsed = cookieparser.parse(req.headers.cookie)
const accessTokenCookie = parsed.access_token
if (!accessTokenCookie) return
const decodedToken = jwtDecode(accessTokenCookie)
if (!decodedToken) return
return decodedToken
}
return null
}
// pages/auth/signup.vue
<script>
import { mapActions } from 'vuex'
export default {
data () {
return {
email: '',
password: '',
renderSource: process.static ? 'static' : (process.server ? 'server' : 'client')
}
},
middleware: ['handle-login-route'],
methods: {
...mapActions('modules/user', [ 'login' ]),
async signUp () {
try {
const firebaseUser = await this.$firebase.auth().createUserWithEmailAndPassword(this.email, this.password)
await this.writeUserData(firebaseUser.user.uid, firebaseUser.user.email)
await this.login(firebaseUser.user)
this.$router.push('/protected')
} catch (error) {
console.log(error.message)
}
},
writeUserData (userId, email) {
const db = this.$firebase.firestore()
return db.collection("Usuarios").doc(userId).set({
email: email
})
}
}
}
</script>
// store/modules/user.js
export const actions = {
async login({ dispatch, state }, user) {
console.log('[STORE ACTIONS] - login')
const token = await this.$firebase.auth().currentUser.getIdToken(true)
const userInfo = {
name: user.displayName,
email: user.email,
avatar: user.photoURL,
uid: user.uid
}
Cookies.set('access_token', token) // saving token in cookie for server rendering
await dispatch('setUSER', userInfo)
await dispatch('saveUID', userInfo.uid)
console.log('[STORE ACTIONS] - in login, response:', status)
},
async logout({ commit, dispatch }) {
console.log('[STORE ACTIONS] - logout')
await this.$firebase.auth().signOut()
Cookies.remove('access_token');
commit('setUSER', null)
commit('saveUID', null)
},
saveUID({ commit }, uid) {
console.log('[STORE ACTIONS] - saveUID')
commit('saveUID', uid)
},
setUSER({ commit }, user) {
commit('setUSER', user)
}
}
Thanks a lot! :D
if you are hosting it on Firebase you should rename your cookie to "__session", that's the only name you could use on Firebase. Their documentation should make it very clear!
Change the following part:
// helpers/index.js
const parsed = cookieparser.parse(req.headers.cookie)
const accessTokenCookie = parsed.__session
// store/modules/user.js
Cookies.set('__session', token)

Domain-wide delegation using default credentials in Google Cloud Run

I'm using a custom service account (using --service-account parameter in the deploy command). That service account has domain-wide delegation enabled and it's installed in the G Apps Admin panel.
I tried this code:
app.get('/test', async (req, res) => {
const auth = new google.auth.GoogleAuth()
const gmailClient = google.gmail({ version: 'v1' })
const { data } = await gmailClient.users.labels.list({ auth, userId: 'user#domain.com' })
return res.json(data).end()
})
It works if I run it on my machine (having the GOOGLE_APPLICATION_CREDENTIALS env var setted to the path of the same service account that is assigned to the Cloud Run service) but when it's running in Cloud Run, I get this response:
{
"code" : 400,
"errors" : [ {
"domain" : "global",
"message" : "Bad Request",
"reason" : "failedPrecondition"
} ],
"message" : "Bad Request"
}
I saw this solution for this same issue, but it's for Python and I don't know how to replicate that behaviour with the Node library.
After some days of research, I finally got a working solution (porting the Python implementation):
async function getGoogleCredentials(subject: string, scopes: string[]): Promise<JWT | OAuth2Client> {
const auth = new google.auth.GoogleAuth({
scopes: ['https://www.googleapis.com/auth/cloud-platform'],
})
const authClient = await auth.getClient()
if (authClient instanceof JWT) {
return (await new google.auth.GoogleAuth({ scopes, clientOptions: { subject } }).getClient()) as JWT
} else if (authClient instanceof Compute) {
const serviceAccountEmail = (await auth.getCredentials()).client_email
const unpaddedB64encode = (input: string) =>
Buffer.from(input)
.toString('base64')
.replace(/=*$/, '')
const now = Math.floor(new Date().getTime() / 1000)
const expiry = now + 3600
const payload = JSON.stringify({
aud: 'https://accounts.google.com/o/oauth2/token',
exp: expiry,
iat: now,
iss: serviceAccountEmail,
scope: scopes.join(' '),
sub: subject,
})
const header = JSON.stringify({
alg: 'RS256',
typ: 'JWT',
})
const iamPayload = `${unpaddedB64encode(header)}.${unpaddedB64encode(payload)}`
const iam = google.iam('v1')
const { data } = await iam.projects.serviceAccounts.signBlob({
auth: authClient,
name: `projects/-/serviceAccounts/${serviceAccountEmail}`,
requestBody: {
bytesToSign: unpaddedB64encode(iamPayload),
},
})
const assertion = `${iamPayload}.${data.signature!.replace(/=*$/, '')}`
const headers = { 'content-type': 'application/x-www-form-urlencoded' }
const body = querystring.encode({ assertion, grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer' })
const response = await fetch('https://accounts.google.com/o/oauth2/token', { method: 'POST', headers, body }).then(r => r.json())
const newCredentials = new OAuth2Client()
newCredentials.setCredentials({ access_token: response.access_token })
return newCredentials
} else {
throw new Error('Unexpected authentication type')
}
}
What you can do here is define ENV variables in your yaml file as described in this documentation to set the GOOGLE_APPLICATION_CREDENTIALS to the path of the JSON key.
Then use a code such as the one mentioned here.
const authCloudExplicit = async ({projectId, keyFilename}) => {
// [START auth_cloud_explicit]
// Imports the Google Cloud client library.
const {Storage} = require('#google-cloud/storage');
// Instantiates a client. Explicitly use service account credentials by
// specifying the private key file. All clients in google-cloud-node have this
// helper, see https://github.com/GoogleCloudPlatform/google-cloud-node/blob/master/docs/authentication.md
// const projectId = 'project-id'
// const keyFilename = '/path/to/keyfile.json'
const storage = new Storage({projectId, keyFilename});
// Makes an authenticated API request.
try {
const [buckets] = await storage.getBuckets();
console.log('Buckets:');
buckets.forEach(bucket => {
console.log(bucket.name);
});
} catch (err) {
console.error('ERROR:', err);
}
// [END auth_cloud_explicit]
};
Or follow an approach similar to the one mentioned here.
'use strict';
const {auth, Compute} = require('google-auth-library');
async function main() {
const client = new Compute({
serviceAccountEmail: 'some-service-account#example.com',
});
const projectId = await auth.getProjectId();
const url = `https://dns.googleapis.com/dns/v1/projects/${projectId}`;
const res = await client.request({url});
console.log(res.data);
}
main().catch(console.error);

How to authorize an HTTP POST request to execute dataflow template with REST API

I am trying to execute the Cloud Bigtable to Cloud Storage SequenceFile template via REST API in a NodeJS backend server.
I am using axios 0.17.1 to send the request and I'm getting 401 status.
I tried to follow the google documentation however I couldn't figure out how to authorize an HTTP request to run a dataflow template.
I want to be authenticated as a service account and I successfully generated and dowloaded the json file containing the private key.
Can anyone help me by showing an example of sending HTTP POST request to https://dataflow.googleapis.com/v1b3/projects/[YOUR_PROJECT_ID]/templates:launch?gcsPath=gs://dataflow-templates/latest/
You need to generate a jwt from your service account credentials. The jwt can be exchanged for an access token which can then be used to make the request to execute the Dataflow job. Complete example:
import axios from "axios";
import jwt from "jsonwebtoken";
import mem from "mem";
import fs from "fs";
const loadServiceAccount = mem(function(){
// This is a string containing service account credentials
const serviceAccountJson = process.env.GOOGLE_APPLICATION_CREDENTIALS;
if (!serviceAccountJson) {
throw new Error("Missing GCP Credentials");
}
})
const loadCredentials = mem(function() {
loadServiceAccount();
const credentials = JSON.parse(fs.readFileSync("key.json").toString());
return {
projectId: credentials.project_id,
privateKeyId: credentials.private_key_id,
privateKey: credentials.private_key,
clientEmail: credentials.client_email,
};
});
interface ProjectCredentials {
projectId: string;
privateKeyId: string;
privateKey: string;
clientEmail: string;
}
function generateJWT(params: ProjectCredentials) {
const scope = "https://www.googleapis.com/auth/cloud-platform";
const authUrl = "https://www.googleapis.com/oauth2/v4/token";
const issued = new Date().getTime() / 1000;
const expires = issued + 60;
const payload = {
iss: params.clientEmail,
sub: params.clientEmail,
aud: authUrl,
iat: issued,
exp: expires,
scope: scope,
};
const options = {
keyid: params.privateKeyId,
algorithm: "RS256",
};
return jwt.sign(payload, params.privateKey, options);
}
async function getAccessToken(credentials: ProjectCredentials): Promise<string> {
const jwt = generateJWT(credentials);
const authUrl = "https://www.googleapis.com/oauth2/v4/token";
const params = {
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
assertion: jwt,
};
try {
const response = await axios.post(authUrl, params);
return response.data.access_token;
} catch (error) {
console.error("Failed to get access token", error);
throw error;
}
}
function buildTemplateParams(projectId: string, table: string) {
return {
jobName: `[job-name]`,
parameters: {
bigtableProjectId: projectId,
bigtableInstanceId: "[table-instance]",
bigtableTableId: table,
outputDirectory: `[gs://your-instance]`,
filenamePrefix: `${table}-`,
},
environment: {
zone: "us-west1-a" // omit or define your own,
tempLocation: `[gs://your-instance/temp]`,
},
};
}
async function backupTable(table: string) {
console.info(`Executing backup template for table=${table}`);
const credentials = loadCredentials();
const { projectId } = credentials;
const accessToken = await getAccessToken(credentials);
const baseUrl = "https://dataflow.googleapis.com/v1b3/projects";
const templatePath = "gs://dataflow-templates/latest/Cloud_Bigtable_to_GCS_Avro";
const url = `${baseUrl}/${projectId}/templates:launch?gcsPath=${templatePath}`;
const template = buildTemplateParams(projectId, table);
try {
const response = await axios.post(url, template, {
headers: { Authorization: `Bearer ${accessToken}` },
});
console.log("GCP Response", response.data);
} catch (error) {
console.error(`Failed to execute template for ${table}`, error.message);
}
}
async function run() {
await backupTable("my-table");
}
try {
run();
} catch (err) {
process.exit(1);
}

Resources