MSAL - use two different clientid from different directory - azure

I have an application that will be used by two different entities and each entity have their own Azure Active Directory.
Initially, the code I am using is:
var msalConfig = {
auth: {
clientId: '<client-id-1>'
authority: "https://login.microsoftonline.com/<tenant-id>"
},
cache: {
cacheLocation: "localStorage",
storeAuthStateInCookie: true
}
};
Now what I want to happen is, can I put two different client ID, and tenant ID?
I could use multiple tenant in the first AAD, but I want to limit it to only two tenants. What should be my approach here?

You could try using the Factory pattern and create a method that will instantiate the clientApplication for the proper client. For example:
const msalConfigFoo = {
auth: {
clientId: '<client-id-1>'
authority: "https://login.microsoftonline.com/<tenant-id>"
},
cache: {
cacheLocation: "localStorage",
storeAuthStateInCookie: true
}
};
var msalConfigBar = {
auth: {
clientId: '<client-id-2>'
authority: "https://login.microsoftonline.com/<tenant-id>"
},
cache: {
cacheLocation: "localStorage",
storeAuthStateInCookie: true
}
};
function getClientApplication(clientType) {
if (clientType == "foo") {
return new Msal.UserAgentApplication(msalConfigFoo);
} else {
return new Msal.UserAgentApplication(msalConfigBar);
}
}

Question: I could use multiple tenant in the first AAD, but I want to limit it to only two tenants. What should be my approach here?
Answer: if you develop a multiple-tenant AD application, you can validate "id_token" with its issuer after your users log in. For example :
var msalConfig = {
auth: {
clientId: 'b0114608-677e-4eca-ae22-60c32e1782d9', //This is your client ID
authority: "https://login.microsoftonline.com/common" //This is your tenant info
},
cache: {
cacheLocation: "localStorage",
storeAuthStateInCookie: true
}
};
var graphConfig = {
graphMeEndpoint: "https://graph.microsoft.com/v1.0/me"
};
// create a request object for login or token request calls
// In scenarios with incremental consent, the request object can be further customized
var requestObj = {
scopes: ["user.read"]
};
var myMSALObj = new Msal.UserAgentApplication(msalConfig);
// Register Callbacks for redirect flow
// myMSALObj.handleRedirectCallbacks(acquireTokenRedirectCallBack, acquireTokenErrorRedirectCallBack);
myMSALObj.handleRedirectCallback(authRedirectCallBack);
// difine issuers
var issuers = new Array();
issuers[0]="https://login.microsoftonline.com/{TenantId}/v2.0";
issuers[1]="https://login.microsoftonline.com/{TenantId}/v2.0";
function signIn() {
myMSALObj.loginPopup(requestObj).then(idToken => {
var issuer =String(idToken.idToken["issuer"])
console.log(issuer)
if(issuers.indexOf(issuer) != -1){
//login successfully then your users can do otherthing
}else{
// your users use a wrong account
}
}).catch(function (error) {
//Please check the console for errors
console.log(error);
});
}
For more details, please refer to the document

Related

disable verification code being sent on email verification on cognito

userPool.signUp(
userData.email,
userData.password,
attributeList,
[],
async (err, result) => {
if (err) {
failure(new HttpException(500, err.message));
}
let myCredentials = new AWS.CognitoIdentityCredentials({
IdentityPoolId: process.env.USER_POOL_ID!,
});
new AWS.Config({
credentials: myCredentials,
region: process.env.AWS_REGION,
});
let cognitoIdentityServiceProvider = new AWS.CognitoIdentityServiceProvider();
cognitoIdentityServiceProvider.adminConfirmSignUp(
{
UserPoolId: process.env.USER_POOL_ID!,
Username: userData.email,
},
function (err, _data) {
if (err) {
failure(err);
}
cognitoIdentityServiceProvider.adminUpdateUserAttributes(
{
UserPoolId: process.env.USER_POOL_ID!,
Username: userData.email,
UserAttributes: [
{
Name: 'email_verified',
Value: 'true',
},
],
},
() => {
console.log('done');
}
);
}
);
if (result) {
const cognitoUser = result.user;
success({ user: cognitoUser, sub: result.userSub });
} else {
failure(
new HttpException(
500,
'Authentication server error, please try again later'
)
);
}
}
);
This is the code by which I am signing up a user and auto verify that user on cognito.
Problem being I have 2 different set of user roles, and one user i am auto confirming but for the other i want them to manually confirm, using the code that is sent by cognito.
Now for the one type of user for which the auto_confirmation is done the email and cofirm user is working perfectly, with one caveat.
The code for verification is being sent even if it's auto verified by admincognito.
How can i disable this on this particualr set of code, so that the other user role can confirm with the code that is being sent via email
Cognito isn't really configurable in this regard. Your escape hatch in this case is the lightly documented custom email sender lambda trigger. Perform whatever checks you want before sending the email through SES (or not).
I solved this issue by creating a pre-signup lambda trigger. And passing in the role to it as a user attribute and then auto verifying email and user based on the role.

How to edit event in google calendar created by service account

I am emailing an HTML link of a Google calendar event generated by Google API to users but they are unable to edit the event, they can only view it when they click the link. I am creating this event with a service account and sharing with other users.
How can I ensure these events are editable in the user's calendar?
This is a link to the code I am using:
const { google } = require('googleapis');
// Provide the required configuration
const CREDENTIALS = JSON.parse(process.env.CREDENTIALS);
const calendarId = process.env.CALENDAR_ID;
// Google calendar API settings
const SCOPES = ['https://www.googleapis.com/auth/calendar.events'];
const calendar = google.calendar({version : "v3"});
const auth = new google.auth.JWT(
CREDENTIALS.client_email,
null,
CREDENTIALS.private_key,
SCOPES,
'email_used_to_configure_the_service_account#gmail.com'
);
auth.authorize(function (err, tokens) {
if (err) {
console.log(err);
return;
} else {
console.log("Successfully connected!");
}
});
//fetching the even object from the db to get evnt.name and co
const saveEvent = {
summary: event.name,
location: event.room.host.location,
description: event.extra,
colorId: 3,
start: {
dateTime: event.startDate,
timeZone: 'Africa/Lagos',
},
end: {
dateTime: event.endDate,
timeZone: 'Africa/Lagos',
},
organizer: {
email: 'email_used_to_configure_the_service_account#gmail.com',
displayName: 'display name',
self: true
},
attendees: [{ email: 'email of recepient of event' }]
//visibility: 'public'
}
async function generateLink(){
try{
const val = await calendar.events.insert({ auth: auth, calendarId: calendarId, resource: saveEvent, sendNotifications: true });
if(val.status === 200 && val.statusText === 'OK'){
console.log('CREATED', val);
return val.data.htmlLink;
}
return console.log('NOT CREATED')
} catch(error){
console.log(`Error ${error}`);
return;
}
}
const link = await generateLink();
let mailData = {
name: user.name ? user.name : user.firstname,
token: `${config.get('platform.url')}/event-accepted/${invite.token}`,
coverImage: event.gallery.link,
eventName: event.name,
hostName: host.name? host.name : host.firstname,
venue: event.venue,
date: event.startDate,
time: event.startDate,
attendees: '',
ticketNo: '',
cost: event.amount,
action: link // htmlLink that takes you to the calendar where user can edit event.
}
mail.sendTemplate({
template: 'acceptEventEmail',
to: u.email.value,
context: mailData
});
Current Behaviour
Expected behaviour
P.S: Code has been added to the question
YOu created the event using a Service account, think of a service account as a dummy user. When it created the event it became the owner / organizer of the event and there for only the service account can make changes to it.
You either need the service account to update it and set someone else as organizer
"organizer": {
"email": "me#gmail.com",
"displayName": "L P",
"self": true
},
Service accounts cannot invite attendees without Domain-Wide Delegation of Authority
In enterprise applications you may want to programmatically access users data without any manual authorization on their part. In Google Workspace domains, the domain administrator can grant to third party applications domain-wide access to its users' data—this is referred as domain-wide delegation of authority. To delegate authority this way, domain administrators can use service accounts with OAuth 2.0.

msal.js v2.3 acquireTokenSilent returning empty access token

I am upgrading an app using msal.js v1.3 to v2.3 and I'm having a problem retreiving the access token once I get my id token.
I initialize the handleRedirectPromise in my constructor. Then, when the user clicks the login button, I call loginRedirect and pass in an object that has the openid scope and the scope from my separately registered api. This works well, the id token comes back and I call acquireTokenSilent to retreive my access token. I pass an object that has my registered api's scope and account from the loginRedirect call into this function.
The problem is that the authorization response from the acquireTokenSilent has an empty access token. The result from the token endpoint looks like:
client_info: "xx"
id_token: "xx"
not_before: 1602895189
refresh_token: "xx"
refresh_token_expires_in: 1209600
scope: ""
token_type: "Bearer"
It doesn't have an access token, but it does specifiy the token type as Bearer
There is no access token in the response and it looks like the scopes property returning is empty. Here is my code:
private msalConfig: Msal.Configuration = {
auth: {
clientId: environment.clientID,
authority: 'https://<tenant>.b2clogin.com/<tenant>.onmicrosoft.com/B2C_1_DefaultSignInSignUp',
knownAuthorities: ['<tenant>.b2clogin.com'],
navigateToLoginRequestUrl: true,
},
cache: {
cacheLocation: 'sessionStorage',
storeAuthStateInCookie: false,
}
};
private loginRequest: Msal.RedirectRequest = {
scopes: ['openid' , 'offline_access', 'https://<tenant>.onmicrosoft.com/api/read' ]
};
private accessTokenRequest: Msal.SilentRequest = {
scopes: ['https://<tenant>.onmicrosoft.com/api/read'] ,
account: null
};
constructor() {
const _this = this;
this.msalInstance = new Msal.PublicClientApplication(this.msalConfig);
this.aquireSilent = (request: Msal.SilentRequest): Promise<Msal.AuthenticationResult> => {
return _this.msalInstance.acquireTokenSilent(request).then(
access_token => {
_this.cacheExpiration(access_token.expiresOn);
_this.isLoggedIn$.next(true);
return access_token;
},
function (reason) {
console.error(reason);
},
);
};
this.msalInstance
.handleRedirectPromise()
.then((tokenResponse: Msal.AuthenticationResult) => {
if (tokenResponse !== null) {
const id_token = tokenResponse.idToken;
const currentAccounts = this.msalInstance.getAllAccounts()
this.accessTokenRequest.account = currentAccounts[0];
this.aquireSilent(this.accessTokenRequest)
}
})
.catch(error => {
console.error(error);
});
}
public login() {
this.msalInstance.loginRedirect(this.loginRequest);
}
Why is the access token not coming back from the token endpoint? Does it have to do with the scopes returning empty? I tried removing the scopes and putting in invalid entries and an error gets raised so I know my request going out is at least valid. Also, just to verify, I have 2 app registrations in AAD, one I created for my spa that has code flow and my older registration I have for my api with an exposed api and scope.
acquireTokenSilent will return an access token only if there is already an entry for that token in the cache. So if for some reason the token was never obtained previously (via loginRedirect, for instance), it will not be able to acquire it silently.
That seems to be the issue in your case. You are mixing scopes for different resources in your loginRequest, and that's perhaps causing the issue in the new version of the library (access tokens are issued per-resource-per-scope(s). See this doc for more) Try modifying your loginRequest object like this:
private loginRequest: Msal.RedirectRequest = {
scopes: ['openid', 'offline_access' ],
extraScopesToConsent:['https://<tenant>.onmicrosoft.com/api/read']
};
Also, the recommended pattern of usage with acquireTokenSilent is that you should fall back to an interactive method (e.g. acquireTokenRedirect) if the acquireTokenSilent fails for some reason.
So I would modify it as:
this.aquireSilent = (request: Msal.SilentRequest): Promise<Msal.AuthenticationResult> => {
return _this.msalInstance.acquireTokenSilent(request).then(
access_token => {
// fallback to interaction when response is null
if (access_token === null) {
return _this.msalInstance.acquireTokenRedirect(request);
}
_this.cacheExpiration(access_token.expiresOn);
_this.isLoggedIn$.next(true);
return access_token;
},
function (reason) {
if (reason instanceof msal.InteractionRequiredAuthError) {
// fallback to interaction when silent call fails
return _this.msalInstance.acquireTokenRedirect(request);
} else {
console.warn(reason);
}
},
);
};
A similar issue is discussed here

Google Calendar API Node JS, can't add attendees with service account

I'm currently trying to develop a simple Node JS program deployed on a GCP Cloud Function, to use Google Calendar API with a Service Account.
The program is simple, i just want to create an event and add attendees.
My code works well, however i can't add attendees with the service account i have this error :
There was an error contacting the Calendar service: Error: Service accounts cannot invite attendees without Domain-Wide Delegation of Authority.
The API are activated and i allowed the calendar scope on the GSuite Admin Security, my service account have the delegation authority checked on GCP.
I search a lot a solution but nothing solve my problem..
This is the code of my module :
const { google } = require('googleapis');
const sheets = google.sheets('v4');
const SCOPES = ['https://www.googleapis.com/auth/spreadsheets','https://www.googleapis.com/auth/calendar'];
let privatekey = require("../creds.json");
async function getAuthToken() {
let jwtClient = new google.auth.JWT(
privatekey.client_email,
null,
privatekey.private_key,
SCOPES);
//authenticate request
jwtClient.authorize(function (err, tokens) {
if (err) {
console.log(err);
return;
} else {
console.log("Successfully connected!");
}
});
return jwtClient;
}
async function insertEvents(auth,items) {
const calendar = google.calendar({ version: 'v3', auth });
var startDate = new Date();
var endDate = new Date();
startDate.setHours(14, 0, 0, 0);
endDate.setHours(15, 0, 0, 0);
var attendees = [];
items.forEach(function(item){
attendees.push({email: item[2]});
});
var event = {
summary: 'Stack Roulette : It\'s Coffee Time =)',
location: 'Partout, mais avec un café',
description: "",
start: {
dateTime: startDate.toISOString(),
timeZone: 'Europe/London'
},
end: {
dateTime: endDate.toISOString(),
timeZone: 'Europe/London'
},
attendees: attendees,
reminders: {
useDefault: false,
overrides: [
{ method: 'email', minutes: 24 * 60 },
{ method: 'popup', minutes: 10 }
]
},
conferenceData: {
createRequest: {requestId: Math.random().toString(36).substring(2, 10),
conferenceSolution: {
key: {
type: "hangoutsMeet"
}
},}
}
};
calendar.events.insert(
{
auth: auth,
calendarId: 'primary',
resource: event,
conferenceDataVersion: 1
},
function(err, event) {
if (err) {
console.log(
'There was an error contacting the Calendar service: ' + err
);
return;
}
console.log('Event created: %s', event.data.htmlLink);
}
);
}
module.exports = {
getAuthToken,
getSpreadSheetValues,
insertEvents
}
I precise, there is no Frontend for my application, the code run with a cloud Function like an API Endpoint.
PS : it's not a Firebase Cloud Function, but GCP Cloud Function
If i remove attendees from event creation, it's work well but can't see the event.
Thx for your help
To set up Domain-Wide Delegation of Authority
Visit your cloud console
Go to IAM & Admin -> Service Accounts
Chose the service account you using in your script
Check Enable G Suite Domain-wide Delegation
If you are sure that you already enabled the domain-wide delegation for the correct service account and you encounter issues only when creating events with attendees:
Your issue is likely related to this issue reported on Google's Issue Tracker:
Currently there are restrictions for creation of events with attendees with a service account, especially when the attendees are outside of your domain.
When you create jwtClient, I think you have to put user primary email to JWT method.
Like:
let jwtClient = new google.auth.JWT(
privatekey.client_email,
null,
privatekey.private_key,
SCOPES,
"yourUserPrimaryEmail"); // here

Not Authorized to access this resource/api while trying to access directory api

I am using node with the googleapis and google-auth-library packages for accessing the users of G-Suite domain. For that a service account was created with the domain-wide-delegation enabled:
The domain admin gave access to the service account to access following scopes:
"https://www.googleapis.com/auth/admin.directory.group.readonly",
"https://www.googleapis.com/auth/admin.directory.group.member.readonly",
"https://www.googleapis.com/auth/admin.directory.user.readonly"
My code looks like this:
import { JWT } from "google-auth-library/build/src/auth/jwtclient";
import * as google from "googleapis";
const keys = require("../google-credentials.json");
async function main() {
const client = new JWT(keys.client_email, undefined, keys.private_key, [
"https://www.googleapis.com/auth/admin.directory.group.readonly",
"https://www.googleapis.com/auth/admin.directory.group.member.readonly",
"https://www.googleapis.com/auth/admin.directory.user.readonly"
]);
await client.authorize();
const service = google.admin("directory_v1");
service.users.list(
{
auth: client,
domain: "my_domain.com",
maxResults: 10,
orderBy: "email"
},
function(err, response) {
if (err) {
console.log("The API returned an error: " + err);
return;
}
var users = response.users;
if (users.length == 0) {
console.log("No users in the domain.");
} else {
console.log("Users:");
for (var i = 0; i < users.length; i++) {
var user = users[i];
console.log("%s (%s)", user.primaryEmail, user.name.fullName);
}
}
}
);
}
main().catch(console.error);
A JWT client get initialised with the credentials received for the service account. Whatever, the client gives the following message back: Not Authorized to access this resource/api
You have to impersonate the service account with an email of a admin of your google domain.
const client = new JWT(
keys.client_email,
undefined,
keys.private_key,
[
"https://www.googleapis.com/auth/admin.directory.group.readonly",
"https://www.googleapis.com/auth/admin.directory.group.member.readonly",
"https://www.googleapis.com/auth/admin.directory.user.readonly"
],
"admin#yourdomain.com"
);
This is mentioned somewhere in the docs in a box, however not really documented anywhere how to implement...

Resources