MS Graph API (mail) issue with POST and scopes - post

Trying to get this to work and GET / Patch work just fine, but POST gives me HTTP STATUS 400 and 403. Must be something with scopes. In Azure AD I have set the following scopes:
Mail.ReadWrite (Delegated)
Mail.ReadWrite (Application)
Mail.Send Delegated)
Mail.Send (Application)
So, signing in works just fine, getting / patching messages as well. Only POST doesnt seem to work.
See code for exact error messages.
Angular10
App.module
export function MSALInstanceFactory(): IPublicClientApplication {
return new PublicClientApplication({
auth: {
clientId: 'xxxx',
authority: 'https://login.microsoftonline.com/common/',
redirectUri: '/',
postLogoutRedirectUri: '/#/login'
},
cache: {
cacheLocation: BrowserCacheLocation.LocalStorage,
storeAuthStateInCookie: isIE, // set to true for IE 11
},
system: {
loggerOptions: {
loggerCallback,
logLevel: LogLevel.Info,
piiLoggingEnabled: false
}
}
});
}
export function MSALInterceptorConfigFactory(): MsalInterceptorConfiguration {
const protectedResourceMap = new Map<string, Array<string>>();
protectedResourceMap.set('https://graph.microsoft.com/v1.0/me', ['user.read', 'mail.readWrite', 'email']);
// also tried these scopes ..
// protectedResourceMap.set('https://graph.microsoft.com/v1.0', ['user.read', 'mail.readWrite', 'email']);
// protectedResourceMap.set('https://graph.microsoft.com/v1.0/query', ['user.read', 'mail.readWrite', 'email']);
// protectedResourceMap.set('https://graph.microsoft.com/v1.0/search/query', ['user.read', 'mail.readWrite', 'email']);
return {
interactionType: InteractionType.Redirect,
protectedResourceMap
};
}
export function MSALGuardConfigFactory(): MsalGuardConfiguration {
return { interactionType: InteractionType.Redirect };
}
#NgModule({
imports: [
BrowserModule,
// etc..
],
declarations: [AppComponent],
providers: [
NgEventBus,
ChhServices,
SynclogService,
AppService,
AuthService,
GapiServices,
{
provide: ErrorHandler,
useClass: ErrorService,
},
{
provide: HTTP_INTERCEPTORS,
useClass: MsalInterceptor,
multi: true
},
{
provide: MSAL_INSTANCE,
useFactory: MSALInstanceFactory
},
{
provide: MSAL_GUARD_CONFIG,
useFactory: MSALGuardConfigFactory
},
{
provide: MSAL_INTERCEPTOR_CONFIG,
useFactory: MSALInterceptorConfigFactory
},
MsalService,
MsalGuard,
MsalBroadcastService
],
bootstrap: [AppComponent],
})
export class AppModule { }
Auth.service
signIn() {
console.log('AuthService::signIn');
this.msalService.loginPopup().subscribe((result) => {
this.accessToken = result['accessToken'];
console.log('authority', result, this.accessToken);
});
}
testGraphApi() {
// 200 OK
const apiGet = this.httpClient.get(`https://graph.microsoft.com/v1.0/me/messages/`).subscribe((data) => {
console.log('get', '/me/messages', data);
});
const categories: any[] = ['custom'];
const body = {
subject: '2320, with tags',
flag: { flagStatus: 'flagged' }, // notFlagged
categories,
body: {
contentType: 'html',
content: 'lalala'
},
inferenceClassification: 'other'
};
const id = 'AQMkADAwATM3ZmYAZS0zOTkANy02MTAwAC0wMAItMDAKAEYAAAM_TfJTK-tISYhjZdaCkkbgBwCPpkVcscQ9QJF-EDzB8h_oAAACAQwAAACPpkVcscQ9QJF-EDzB8h_oAAACHbIAAAA=';
// 200 OK
const apiPatch = this.httpClient.patch(`https://graph.microsoft.com/v1.0/me/messages/${id}`, body).subscribe((data) => {
console.log('patch', '/me/messages', data);
});
const bodySendMail = {
'message': {
'subject': 'Meet for lunch?',
'body': {
'contentType': 'Text',
'content': 'The new cafeteria is open.'
},
// etc..
}
}
const headers = new HttpHeaders({ 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.accessToken}` });
// 403 Forbidden
// "code": "ErrorAccessDenied",
// "message": "Access is denied. Check credentials and try again.",
const apiSendMail = this.httpClient.post(`https://graph.microsoft.com/v1.0/me/sendMail`, bodySendMail, { headers }).subscribe((data) => {
console.log('post', '/me/sendMail', data);
});
const bodySearch = {
'requests': [
{
'entityTypes': [
'message'
],
'query': {
'queryString': 'ref:6019d6bf1ce3425fb833559e'
},
'from': 0,
'size': 5
}
]
}
// 400 Bad Request
// "code": "AuthenticationError",
// "message": "Error authenticating with resource",
const apiSearch = this.httpClient.post(`https://graph.microsoft.com/v1.0/search/query`, bodySearch, { headers }).subscribe((data) => {
console.log('post', '/search/query', data);
});
}

// 403 Forbidden
// "code": "ErrorAccessDenied",
// "message": "Access is denied. Check credentials and try again."
Send mail API needs Mail.Send permission. When requesting /me endpoint which bases the current signed-in user, it should have the delegated permission.
So you need to add Mail.Send of delegated permission in the portal and add it in your code.
// 400 Bad Request
// "code": "AuthenticationError",
// "message": "Error authenticating with resource"
searchEntity: query API needs the Mail.ReadWrite delegated permission. This api only supports "work or school account". A work account typically uses an organization’s custom domain name or company name, such as "jon#contoso.com" or "xxx#yourTenantName.onmicrosoft.com".
You could test to request the api in Graph Explorer.

Related

Youtube CommentThreads api Insert not working

I'm trying to Use Youtube api v3 to comment on video and getting this error but my request data is correct according to documentation.
Here is my code.
Using oauth the code setting access_token like this
oauth.setCredentials(tokens);
var channelId = "UCq-Fj5jknLsUf-MWSy4_brA";
var request = Youtube.commentThreads.insert({
"part": [
"snippet"
],
"resource": {
"snippet": {
"videoId": "qfuFeUnAm8E",
"topLevelComment": {
"snippet": {
"textOriginal": "best video"
}
},
"channelId": channelId
}
}
}, (err, data) => {
if (err) {
console.log(err, 'errerrerr')
}
if (data) {
console.log(data, 'datadata');
}
});
This is the error i'm getting in return
errors: [
{
message: "The API server failed to successfully process the request. While this can be a transient error, it usually indicates that the request's input is invalid. Check the structure of the <code>commentThread</code> resource in the request body to ensure that it is valid.",
domain: 'youtube.commentThread',
reason: 'processingFailure',
location: 'body',
locationType: 'other'
}
]
This is the authentication or authorization code generating everytime
"tokens": {
"access_token": "[redacted]",
"scope": "https://www.googleapis.com/auth/youtube https://www.googleapis.com/auth/youtubepartner https://www.googleapis.com/auth/youtube.force-ssl https://www.googleapis.com/auth/youtube.upload",
"token_type": "Bearer",
"expiry_date": 1655195240477
}
I think you should consider testing out your insert in the try me The object you have created doesnt look right at all.
You should consult comments resource for the proper format of the body.
<script src="https://apis.google.com/js/api.js"></script>
<script>
/**
* Sample JavaScript code for youtube.comments.insert
* See instructions for running APIs Explorer code samples locally:
* https://developers.google.com/explorer-help/code-samples#javascript
*/
function authenticate() {
return gapi.auth2.getAuthInstance()
.signIn({scope: "https://www.googleapis.com/auth/youtube.force-ssl"})
.then(function() { console.log("Sign-in successful"); },
function(err) { console.error("Error signing in", err); });
}
function loadClient() {
gapi.client.setApiKey("YOUR_API_KEY");
return gapi.client.load("https://www.googleapis.com/discovery/v1/apis/youtube/v3/rest")
.then(function() { console.log("GAPI client loaded for API"); },
function(err) { console.error("Error loading GAPI client for API", err); });
}
// Make sure the client is loaded and sign-in is complete before calling this method.
function execute() {
return gapi.client.youtube.comments.insert({
"resource": {
"snippet": {
"videoId": "qfuFeUnAm8E",
"channelId": "UCq-Fj5jknLsUf-MWSy4_brA"
}
}
})
.then(function(response) {
// Handle the results here (response.result has the parsed body).
console.log("Response", response);
},
function(err) { console.error("Execute error", err); });
}
gapi.load("client:auth2", function() {
gapi.auth2.init({client_id: "YOUR_CLIENT_ID"});
});
</script>
<button onclick="authenticate().then(loadClient)">authorize and load</button>
<button onclick="execute()">execute</button>

SignIn callback error when using NextAuth with oauth_get_access_token_error and oauth_callback_error

I am trying to add custom oauth provider to my next.js app. I am adding custom provider in [...nextauth].js:
export default NextAuth({
// Configure one or more authentication providers
providers: [
{
id: "moneybutton",
name: "Money Button",
type: "oauth",
version: "2.0",
scope: "auth.user_identity:read users.profiles:read users.profiles.email:read users.balance:read",
params: {
grant_type: "authorization_code"
},
accessTokenUrl: "https://www.moneybutton.com/oauth/v1/token",
requestTokenUrl: "https://www.moneybutton.com/oauth/v1/token",
authorizationUrl: "https://www.moneybutton.com/oauth/v1/authorize?response_type=code",
profileUrl: "https://www.moneybutton.com/api/v1/auth/user_identity",
profile(profile) {
return {
id: profile.data.attributes.id,
name: profile.data.attributes.name,
};
},
clientId: 'my_oauth_identifier',
clientSecret: 'my_client_secret'
}
// ...add more providers here
],
debug: true
});
OAuth flow seems to work correct, as i am seeing my profile id coming back in responses but it finishes on http://localhost:3000/api/auth/signin?error=Callback
I set debug to true and i am getting following errors:
[next-auth][error][oauth_get_access_token_error]
https://next-auth.js.org/errors#oauth_get_access_token_error {
statusCode: 400,
data: '{"errors":[{"id":"6da534f0-a512-11eb-92e8-891975d02f44","status":400,"title":"Bad Request","detail":"Invalid client: client is invalid"}],"jsonapi":{"version":"1.0"}}'
} undefined undefined
[next-auth][error][oauth_get_access_token_error]
https://next-auth.js.org/errors#oauth_get_access_token_error {
statusCode: 400,
data: '{"errors":[{"id":"6da534f0-a512-11eb-92e8-891975d02f44","status":400,"title":"Bad Request","detail":"Invalid client: client is invalid"}],"jsonapi":{"version":"1.0"}}'
} moneybutton 9f3970b8ae39f9d46f9fae56f6fb6135ecb7e87b
[next-auth][error][oauth_callback_error]
https://next-auth.js.org/errors#oauth_callback_error {
statusCode: 400,
data: '{"errors":[{"id":"6da534f0-a512-11eb-92e8-891975d02f44","status":400,"title":"Bad Request","detail":"Invalid client: client is invalid"}],"jsonapi":{"version":"1.0"}}'
It says that client is invalid, but i am sure oauth identifier and secret are correct, as well as redirect URL set to http://localhost:3000/api/auth/callback/moneybutton.
response for profile looks like this if it would be helpful:
{
"data": {
"id": "75101",
"type": "user_identities",
"attributes": {
"id": "75101",
"name": "John Doe"
}
},
"jsonapi": {
"version": "1.0"
}
}
Links to documentations:
https://next-auth.js.org/configuration/providers
https://docs.moneybutton.com/docs/api/v1/api-v1-user-identity
I don't know if it's some bug or my approach is wrong and will apreciate any help

actions on google, Oauth account Linking

I have been trying to connect an assistant action to my backend server
I am using my own Oauth server and followed the instructions on
https://developers.google.com/actions/identity/oauth2?oauth=code
I am using actions_intent_Sign_in for my dialogflow event intent (like https://actions-on-google.github.io/actions-on-google-nodejs/classes/conversation_helper.signin.html)
when i use my action to sign in, i get the login window to my server, i do the account linking and i can see that i generated the tokens on my server but i cant find the token in (conv.user.access.token)
and this is the code for my intent using "actions on google sdk "
'use strict';
var _ = require('lodash');
var path = require('path')
var express = require('express')
var http = require('http')
const bodyParser = require('body-parser');
var expressApp = express().use(bodyParser.json());
var server = http.createServer(expressApp).listen(3000)
const {
dialogflow,
SignIn
} = require('actions-on-google');
const app = dialogflow({
debug: true,
clientId: '7b4a6dfc-4b35-11e9-8646-d663bd873d93'
});
app.intent('Start Sign-in', conv => {
conv.ask(new SignIn());
});
app.intent('Get Sign-in', (conv, params, signin) => {
console.log("get sign in ");
console.log(JSON.stringify(signin));
if (signin.status === 'OK') {
const access = conv.user.access.token
console.log("the access token is " + access);
conv.ask('Great, thanks for signing in! What do you want to do next?');
} else {
conv.ask('I wont be able to save your data, but what do you want to do next?.');
}
});
and the response comes back as
{"#type":"type.googleapis.com/google.actions.v2.SignInValue","status":"OK"}
the access token is undefined
Response {
"status": 200,
"headers": {
"content-type": "application/json;charset=utf-8"
},
"body": {
"payload": {
"google": {
"expectUserResponse": true,
"richResponse": {
"items": [
{
"simpleResponse": {
"textToSpeech": "Great, thanks for signing in! What do you want to do next?"
}
}
]
}
}
}
}
}
the user object of conv has only this data
"user": {
"raw": {
"lastSeen": "2019-03-20T12:46:23Z",
"locale": "en-US",
"userId": "okdhyeGSk5tofgLjEepIUrA6mmewCESY8MjklZRPvQJgv6-uybfPobwdfgtrGZJ3bE2sM9ninhst"
},
"storage": {},
"_id": "okdhyeGSk5tofgLjEepIUrA6mmewCESY8MjklZRPvQJgv6-uybfPobwdfgtrGZJ3bE2sM9ninhst",
"locale": "en-US",
"permissions": [],
"last": {
"seen": "2019-03-20T12:46:23.000Z"
},
"name": {},
"entitlements": [],
"access": {},
"profile": {}
}
i dont know where the access/refresh token can be found or if there is any requirement for the post to send from my oauth server that i missed
so finally i managed to get it working with the help of Actions on Google Support Team
the problem was me having another google account logged-in in another tab, even though i had the AoG and dialogflow agent connected with the same account
tried all using incognito window and it works

Error creating live broadcast - NodeJS libs

I'm trying to create a live broadcast using the nodejs client library but I'm getting the following error:
{ Error: Title is required
at Request._callback code: 400,
.
.
.
errors:
[ { domain: 'youtube.liveBroadcast',
reason: 'titleRequired',
message: 'Title is required',
extendedHelp: 'https://developers.google.com/youtube/v3/live/docs/liveBroadcasts/insert#request_body' } ] }
It's working on the API Explorer and that's getting me lost with this one. Here is code:
var google = require('googleapis');
var OAuth2 = google.auth.OAuth2;
var youtube = google.youtube('v3');
var oauth2Client = new OAuth2(
'xxxx', //CLIENT_ID
'xxxx', //MY_CLIENT_SECRET,
'http://localhost:3000/api/integrations/youtube'//YOUR_REDIRECT_URL
);
oauth2Client.setCredentials({
access_token: "xxxx",
refresh_token: "xxxx"
});
broadcastParams = {
"auth": oauth2Client,
"part": "snippet,status,contentDetails",
"snippet": {
"title": "Testing NodeJS",
"scheduledStartTime": "2017-02-20T14:00:00.000Z",
"scheduledEndTime": "2017-02-20T15:00:00.000Z",
},
"status": {
"privacyStatus": "private",
},
"contentDetails": {
"monitorStream": {
"enableMonitorStream": true,
}
}
};
youtube.liveBroadcasts.insert(broadcastParams,
function(err,broadcast) {
if (err) {
return console.log('Error creating broadcast: ', err);
}
console.log('Broadcast = ' + JSON.stringify(broadcast));
});
Thanks for the help!
Got it solved.
My broadcast parameters wasn't correct. I was missing the "resource". Here is the code that is working now:
var google = require('googleapis');
var OAuth2 = google.auth.OAuth2;
var youtube = google.youtube('v3');
var oauth2Client = new OAuth2(
'xxxx', //CLIENT_ID
'xxxx', //MY_CLIENT_SECRET,
'http://localhost:3000/api/integrations/youtube'//YOUR_REDIRECT_URL
);
oauth2Client.setCredentials({
access_token: "xxxx",
refresh_token: "xxxx"
});
broadcastParams = {
"auth": oauth2Client,
"part": "snippet,status,contentDetails",
"resource": {
"snippet": {
"title": "Tesing NodeJS 123",
"scheduledStartTime": "2017-02-20T14:00:00.000Z",
"scheduledEndTime": "2017-02-20T15:00:00.000Z",
},
"status": {
"privacyStatus": "private",
},
"contentDetails": {
"monitorStream": {
"enableMonitorStream": true,
}
}
}
};
youtube.liveBroadcasts.insert(broadcastParams, function(err,broadcast) {
if (err) {
return console.log('Error creating broadcast: ', err);
}
console.log('Broadcast = ' + JSON.stringify(broadcast));
});

loopback.io rest connector - how to pass through oAuth token

Using loopback, I have created a connection to an existing API using the REST connector, which is working well. I would however like to pass through the oAuth token coming from the client.
I can get hold of the oAuth token by grabbing ctx.req.headers.authorization from the Model.beforeRemote method, but can't seem to figure out a way of passing it to the REST connector as a new header.
I've tried a couple of things:
Adding a hook using Model.observe (but this doesn't seem to fire with the REST connector).
Using a template with an authorization field - but have not been able to get this working correctly.
Any ideas appreciated.
With the connector below you should be able to pass the OAuth token into the function (as first parameter in the example). Does something like this not work for you?
{
connector: 'rest',
debug: false,
options: {
"headers": {
"accept": "application/json",
"content-type": "application/json",
"authorization": "{oauth}"
},
strictSSL: false,
},
operations: [
{
template: {
"method": "GET",
"url": "http://maps.googleapis.com/maps/api/geocode/{format=json}",
"query": {
"address": "{street},{city},{zipcode}",
"sensor": "{sensor=false}"
},
"options": {
"strictSSL": true,
"useQuerystring": true
},
"responsePath": "$.results[0].geometry.location"
},
functions: {
"geocode": ["oauth", "street", "city", "zipcode"]
}
}
]}
Wanted to answer this, and build on Bryan's comments. Firstly, in datasources.json, you'll want to setup the REST connector:
{
"name": "connect",
"connector": "rest",
"debug": "true",
"operations": [
{
"template": {
"method": "GET",
"url": "http://server/api",
"headers":{
"authorization": "Bearer {token}"
}
},
"functions": {
"get": ["token"]
}
}
]
}
As Bryan covered, it possible to put the auth header in each call, or at the root of the connector.
Secondly, and this is the bit I was stuck on, in order to pass the token to the API call from a model, it's required to generate a remote method that passes the token as a query parameter. This is what it looks like in this example:
module.exports = function (Model) {
Model.disableRemoteMethod('invoke', true);
Model.disableRemoteMethod('get', true);
Model.call = function (req, cb) {
var token = req.token;
Model.get(token, function (err, result) {
cb(null, result);
});
};
Model.remoteMethod(
'call',
{
http: {path: '/', verb: 'get'},
accepts: [
{arg: 'req', type: 'object', http: {source: 'req'}}
],
returns: {
root: true
}
}
);
};
Notice how the req argument is required in order to provide the request to the model. You also notice that I've disabled the original get and invoke methods (replacing it with a more REST-friendly resource).
Finally, you'll need to get the token into the request. For this, it's easy enough to use some middleware. Here's an example from server.js:
app.use('/api', function (req, res, next) {
oidc.authenticate(req, function (err, token) {
if (err) {
return res.send({status: 401, message: err});
}
req.token = token;
next();
});
});
In the above example, I'm using an internal OIDC provider to validate the token, but of course, you can use anything.

Resources