Authorization Denied trying to access Bookings Api in Microsoft Graph - azure-active-directory

I am trying to create an app that runs from an AWS lambda that acts as a middle man for customers wanting to sign up for a booking using Microsoft Bookings. Following the documentation I am able to generate an access token for Graph, but I get an authorization denial when I try to request information from bookings through the api.
My Code:
import request from "request";
import { Callback } from "./callback";
const APP_ID = process.env.BOOKINGS_APP_ID;
const APP_SECRET = process.env.BOOKINGS_API_SECRET;
const TOKEN_ENDPOINT = `https://login.microsoftonline.com/${process.env.BOOKINGS_TENANT_NAME}.onmicrosoft.com/oauth2/token`;
const requestParams = {
client_id: APP_ID,
client_secret: APP_SECRET,
grant_type: "client_credentials",
resource: "https://graph.microsoft.com",
scope: "https://graph.microsoft.com/.default",
};
export default class Bookings {
public static async listBusinesses(callback: Callback) {
Bookings.generateAPIToken((err: string | null, data: any) => {
const options = {
// body: {},
headers: {
"Authorization": `Bearer ${data}`,
"Content-Type": "application/json",
},
method: "GET",
url: "https://graph.microsoft.com/beta/bookingBusinesses",
};
console.log(data);
return request(options, (error: string, res: any) => {
if (error) {
return callback(error, {});
}
return callback(null, JSON.parse(res.body));
});
});
}
public static async generateAPIToken(callback: Callback) {
request.post({ url: TOKEN_ENDPOINT, form: requestParams }, (err, res, body) => {
if (err) {
return callback(err, {});
}
return callback(null, JSON.parse(body).access_token);
});
}
}
The error that I get:
{
error: {
code: '',
message: 'Authorization has been denied for this request.',
innerError: {
'request-id': '695d3f90-357d-490c-b980-4a2018dd39a5',
date: '2020-06-08T03:21:59'
}
}
}
I have also tried using the microsoft-graph-client library to access bookings but that doesn't work either. What I am doing wrong? Thank you for any help.

We can see the document shows the graph api(bookingBusinesses) which you want to request requires delegated type permissions and not support application type permission.
So we can not use "client_credentials" grant flow, your code shows you use "client_credentials" as the grant type. You can use "username/password" grant flow to get the access token instead. So the param you request for the access token should be like below:
const requestParams = {
client_id: APP_ID,
client_secret: APP_SECRET,
grant_type: "password",
scope: "https://graph.microsoft.com/.default",
username: "your user name/email(like xxxxx#xxx.onmicrosoft.com)",
password: "your password"
};
By the way, I noticed the "TOKEN_ENDPOINT" in your code is https://login.microsoftonline.com/${process.env.BOOKINGS_TENANT_NAME}.onmicrosoft.com/oauth2/token and you use both params resource and scope in requestParams. If we use v1 endpoint as your code, we just need to use the param resource. If we use v2 endpoint(https://login.microsoftonline.com/${process.env.BOOKINGS_TENANT_NAME}.onmicrosoft.com/oauth2/v2.0/token), we need to use use the param scope instead of the param resource. The code I provided above use v2, so I use scope param and you also need to change the "TOKEN_ENDPOINT" to v2(just add a v2.0 between the oauth2/ and /token).
If you don't want to change the "TOKEN_ENDPOINT" to v2, just use the params like below:
const requestParams = {
client_id: APP_ID,
client_secret: APP_SECRET,
grant_type: "password",
resource: "https://graph.microsoft.com",
username: "your user name/email(like xxxxx#xxx.onmicrosoft.com)",
password: "your password"
};
Hope it helps~

Related

How do I access JSON component only?

I want to access only the component within a particular JSON header. The fetch code I have written is rendering a JSON output along with the variable name.
I wrote a fetch api and I am returning the response as follows:
fetchPreAuthnConfig: async (data) => {
try {
const response = await fetch(preAuthnEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
const configurations = await response.json();
console.log(configurations);
return configurations;
} catch (error) {
console.error('Error', error);
return error;
}
},
But the json is returned along with the configurations header which is breaking my functionality on the other code block. The JSON response is as follows:
configurations:
appClientId: "0oa7xxxxxxxxxxxx"
applicationId: "0oa7xxxxxxxxxxxx"
applicationLabel: "Application Name"
createAccount: false
displayName: "Application Name"
emailToken: "{}"
helpTemplateName: "help"
hideResetAccountOption: false
rewriteUrl: ""
scope: "openid profile invites"
serverTime: 1654682083399
termsOnPasswdPage: false
title: "Login"
workflow: "Authorize"
workflowHandlerEndpoint: ""
workflowType: "LOGIN"
How can I refactor the above code so that the returned JSON is:
appClientId: "0oa7xxxxxxxxxxxx"
applicationId: "0oa7xxxxxxxxxxxx"
applicationLabel: "Application Name"
createAccount: false
displayName: "Application Name"
emailToken: "{}"
helpTemplateName: "help"
hideResetAccountOption: false
rewriteUrl: ""
scope: "openid profile invites"
serverTime: 1654682083399
termsOnPasswdPage: false
title: "Login"
workflow: "Authorize"
workflowHandlerEndpoint: ""
workflowType: "LOGIN"
Basically remove the configurations header.
You need to access the key configurations in the JSON object, you can do it as follows:
const { configurations } = await response.json()

How can I add a CORS header to this Lambda function

Please bare with me as this is my first stack overflow post, but I have minimal backend experience and am really struggling to meet CORS requirements.
I want to use AWS (SES, API Gateway, Lambda) to send form data to our company email account. My function works currently when testing in AWS, but it doesn't work on the client side of my site. From what I've gathered from research so far, my Lambda function needs a CORS header to work. Here is the code:
var aws = require("aws-sdk");
var ses = new aws.SES({ region: "us-east-1" });
exports.handler = async function(payload) {
var params = {
Destination: {
ToAddresses: ['placeholder#place.com'],
},
Message: {
Body: {
Text: {
Data: `\n
${payload.fullName} has tried to contact you. \n
Message: \n
-------------------- \n
${payload.comments} \n
-------------------- \n
Here is the sender's contact information: \n
Name: ${payload.fullName} \n
Email: ${payload.emailAddress} \n
Phone: ${payload.phone} \n
Company: ${payload.companyName}`
},
},
Subject: { Data: payload.subject },
},
Source: 'placeholder#place.com',
};
return ses.sendEmail(params).promise()
};
I'm looking at this code as an example of how to include a CORS header:
exports.handler = async (event) => {
let data = {};
let res = {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*' // replace with hostname of frontend (CloudFront)
},
body: JSON.stringify(data)
};
return res;
};
Can anyone help me to combine these two approaches? I don't understand how to make the SES function into a more traditional response. I am mostly a frontend dev, so I expect that I'm missing something silly. I appreciate any responses though.
If you can change the API Gateway integration type to Lambda Proxy, then this code can help you.
Move the entire code in the handler method to another function say sendEmail
const sendEmail = async function(payload) {
// Your code to crete the `params` omitted for brevity
return ses.sendEmail(params).promise()
};
The handler can call this function and based on the outcome of this function send an appropriate result with the CORS headers
exports.handler = async function(event) {
const payload = JSON.parse(event.body);
const CORS_HEADERS = {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*' // Your origin name
};
try {
await sendEmail(payload);
return {
statusCode: 200,
headers: CORS_HEADERS,
body: '{}'
}
} catch(err) {
return {
statusCode: 500, // Can be 4XX or 5XX depending on the error
headers: CORS_HEADERS,
body: `{"err": ${err.messge}}`
}
}
}
For this to work for CORS requests, you also need to ensure the OPTIONS request responds with appropriate headers. You can do so using the AWS console following this documentation. For CloudFormation along with api-gateway V2, this documentation should help. For AWS SAM, this documentation should help (If you are not already using any Serverless development tool, take a look at AWS SAM).
If you don't wish to use the Lambda proxy, then ensure the integration response send the appropriate CORS headers for both the OPTIONS request and the POST request. This can help.

Update user in AD B2C using Microsoft Graph API (error 400 bad request)

I have collection of users in AD B2C and now using Graph API I want to update one of the custom attributes of user, but constantly I have error 400 Bad Request
This is my code
const test = [
{ client: 'john', status: 'confirmed' },
{ client: 'ema', status: 'rejected' },
{ client: 'melissa', status: 'pending' },
{ client: 'alice', status: 'pending' },
{ client: 'richard', status: 'pending' }
]
const options2 = {
method: 'PATCH',
headers: {
Authorization: 'Bearer ' + token,
'content-type': 'application/json'
},
body: JSON.stringify({ extension_{applicationID}_Clients: JSON.stringify(test) })
}
fetch(
'https://graph.microsoft.com/beta/users/' + decoded.oid,
options2
)
.then(
(result) => {
console.log('result update user', result)
},
(error) => {
console.log(error)
}
)
This is the response
[Symbol(Response internals)]: {
url: 'https://graph.microsoft.com/beta/users/{userID}',
status: 400,
statusText: 'Bad Request',
headers: Headers { [Symbol(map)]: [Object: null prototype] },
counter: 0
}
}
I would really appreciate any kind of help to solve this problem.
I suppose body is the problem but I'm not sure where exactly.
EDIT: I'm saving stringfied array into users object
I tried and tested updating a custom attribute in B2C for a user using Graph API and it worked for me. Let me share the steps:
Create a custom attribute in B2C under User attributes blade:
Now in order to update/patch a user with custom attribute, you need to use the following convention extension_appIdOfB2CExtensionApp_CustomAttributeName. Make sure the appId of the B2C Extension app has no hypens when when you use it with the convention extension_appIdOfB2CExtensionApp_CustomAttributeName. More details can be found here.
I used Postman and made a PATCH call and updated the user custom attribute for the B2C user.
Used the GET call and listed the user attributes and found the newly added custom attribute in the user profile:

How to get user.email in server using auth0 and node.js?

On my frontend, I can see the email through:
console.log(auth.profile.email)
Then I call this service to retrieve some information from the backend, it's protected so you need to be logged in to get anything back:
var metadata_req = {
method: "GET",
url: "http://localhost:80/protected/all/metadata"
}
On the backend, I'm using node.js and this to verify that the user is logged in before doing a request:
var jwtCheck = express-jwt({
secret: new Buffer(config.secret, 'base64'),
audience: config.client_id
});
But if I print out req.user, I only see iss, sub, aud, exp and iat. I would like to see the email as well.
You can get profile information in req.user by including the email permission when your user initially logs in.
For example, using lock in angular would look like this:
auth.signin({
authParams: {
scope: 'openid email'
}
}, function(profile, token) {
// stuff
}, function(err) {
// error handling
});
Change your API definition from "all/metadata" to "all/metadata/:emailId"
Call your API
var metadata_req = {
method: "GET",
url: "http://localhost:80/protected/all/metadata/{your_email_id}"
}
In NodeJS retrieve you email_id from req.params.emailId

howto prevent www-authenticate header when using passport-http Basic + passport-local combination

Hi I want to support both formbased authentication and http basic authentication in my app. Everything works as expected except when I use form based auth via angularjs with wrong credentials.
Instead of having my angular code handle the 401, the browser shows the BASIC auth dialog, caused by the WWW-Authenticate header.
How can I prevent that header from being added when the local strategy is used?
Or how can I support both mechanisms in a different way?
I use the following route in my express based app.
api.post('/authenticate', passport.authenticate(['local', 'basic'], { session: false }), function (req, res) {
This enables both authentication methods on that url. I repeat, when I use wrong credentials using formbased it shows me the basic auth dialog (I don't want that).
Following is how I registered the strategies.
passport.use(new BasicStrategy({ realm: 'Authentication failed. Wrong username or password.'}, verifyLocalUser));
passport.use(new LocalStrategy(verifyLocalUser));
This is how my verifyUser method looks like...
var verifyLocalUser = function (username, password, next) {
User.findOne({
username: username
}).select('fullname admin username password').exec(function (err, user) {
if (err) {
return next(err);
}
if (user && user.comparePasswords(password)) {
return next(null, user);
} else {
next(null, false, { message: 'Authentication failed. Wrong username or password.' });
}
});
}
Does anyone know how to support multiple authentication methods using passport.js?
For completeness, this is the angular code which authenticates me...
authFactory.signIn = function (username, password) {
return $http.post('/api/authenticate', {
username: username,
password: password
}).then(function (res) {
AuthToken.setToken(res.data.token);
return res.data;
}, function (res) {
console.warn(res);
});
};
instead of this:
next(null, false, { message: 'Authentication failed. Wrong username or password.' });
You can use this:
cb(new YourCustomError())
And "YourCustomError" can have a message, for me mine "YourCustomError" looks like:
class HttpError extends Error {
constructor (msg = 'Invalid Request', status = 400) {
super(msg)
this.status = status
}
}
class Forbidden extends HttpError {
constructor (msg = 'Forbidden') {
super(msg, 403)
}
}
Or probably new Error(<message>) will work properly for you, too

Resources