How Setup Azure AD and Cypress so SSO Login works - azure

I try to automate testing with cypress.
For the app to work the user needs to login.
So i try to automate this step.
I wrote following command which i pass in the username password.
Cypress.Commands.add('login', ( userId, pwd ) => {
const domain = 'XXXX'
const clientId = 'XXXX'
const clientSecret = 'XXXX'
const scope = 'XXXX'
cy.request({
method:'POST',
url:`https://login.microsoftonline.com/${domain}/oauth2/v2.0/token`,
header:
{
'cache-control': 'no-cache',
'Content-Type': 'application/x-www-form-urlencoded'
},
form: true,
body: {
grant_type: 'password',
client_id: clientId,
client_secret: clientSecret,
password: pwd,
scope: scope,
username: userId
}
})
.then((resp)=>{
const token = resp.body['access_token']
console.log(token)
//cy.visit('http://{URL}/auth/#access_token='+token)
})
})
With this i get the error message back. That the Username or password is wrong.
Anyone knows how to fix this? or know a better way to setup Cypress with a SSO from azure AD?

The Azure AD ROPC flow should work, when using it, make sure you are using a work account whose format is xxx#xxx.onmicrosoft.com, and it is not MFA-enabled, details see screenshot below. If you don't have such an account, follow this to create one.
In your case, please double-check the username and password, make sure they are correct.(If they are correct, please make sure there is no escape issue in the actual request.)
I test the flow in the postman, it works fine.

Related

Azure B2C login with Cypress using cy.request()

Our app requires Azure b2c authentication when login in. I automated the login by simply typing the username and password. In addition I had to add "chromeWebSecurity": false to avoid cross-origin issue.
I think this is the incorrect way of login in. While I was searching for a solution I came across some articles regarding this. Apparently we could user Cy.request() and get the response and then access the app.
However, I was unable to implement this.
Has anyone implemented the Azure b2c login automation with cypress ? if so can someone explain how this needs to be done ?
Our app uses access bearer tokens.
It first sends authentication requests
GET https://{tenant}.b2clogin.com/{tenant}.onmicrosoft.com/oauth2/v2.0/authorize
Then gets the token
POST https://{tenant}.b2clogin.com/{tenant}.onmicrosoft.com/oauth2/v2.0/token
Thanks
I'm not too familiar with Azure b2c authentication, but looks like the general pattern is given here How to use Cypress to test your SharePoint solution with an Azure AD-secured API
The bulk of the code is this
Cypress.Commands.add("visitWithAdal", (pageUrl) => {
const config = {
username: process.env.CI ? Cypress.env('USERNAME') : Cypress.env('username'),
password: process.env.CI ? Cypress.env('PASSWORD') : Cypress.env('password'),
tenant: process.env.CI ? Cypress.env('TENANT') : Cypress.env('tenant'),
clientId: process.env.CI ? Cypress.env('CLIENTID') : Cypress.env('clientid'),
clientSecret: process.env.CI ? Cypress.env('CLIENTSECRET') : Cypress.env('clientsecret'),
resource: process.env.CI ? Cypress.env('RESOURCE') : Cypress.env('resource')
};
// Fetch the access token for the Microsoft Graph
cy.request({
method: 'POST',
url: `https://login.microsoft.com/${config.tenant}/oauth2/token`,
header: {
'cache-control': 'no-cache',
'Content-Type': 'application/x-www-form-urlencoded'
},
form: true,
body: {
grant_type: 'password',
client_id: config.clientId,
client_secret: config.clientSecret,
resource: config.resource,
password: config.password,
username: config.username
}
}).then(response => {
if (response && response.status === 200 && response.body) {
const accessToken = response.body["access_token"];
const expires = response.body["expires_on"];
// Store the retrieved access token in the session storage
cy.window().then((crntWindow) => {
crntWindow.sessionStorage.setItem(`adal.token.keys`, `${config.resource}|`);
crntWindow.sessionStorage.setItem(`adal.expiration.key${config.resource}`, expires);
crntWindow.sessionStorage.setItem(`adal.access.token.key${config.resource}`, accessToken);
cy.visit(pageUrl);
});
}
});
});

Modern Oauth2 authentication for sending mails using Nodemailer nodejs

I am using nodemailer to send email in my nodejs application.
var payload = { auth:
{
user: smtpuser,
pass: smtppass
},
to : toAddr,
from : emailfrom,
cc : ccAddr,
subject : subject,
html : content,
attachments: attachments
};
var transporter = nodemailer.createTransport(
{ host: payload.host || 'smtp.office365.com', // Office 365 server
port: payload.port || 587, // secure SMTP
secure:payload.secure || false, // false for TLS - as a boolean not string - but the default is false so just remove this completely
auth: payload.auth,
debug: true,
tls: payload.tls || {ciphers: 'SSLv3'}
});
transporter.sendMail(payload, function (error, info) {
if (error) {
return console.log(error);
}
updateMessage(updatedMsg);
});
I started getting this error:
Error: Invalid log in: 535 5.7.3 Authentication unsuccessful [SN4PR0601CA0002.namprd06.prod.outlook.com]
It seems my team has now disabled basic authentication.
I need to implement modern authentication(Oauth2) to be able to send mails via nodemailer using the outlook id.
Does anyone have any idea about this?
What configuration(code) changes will that require ?
After long time discovering how to send an email from your server with OAuth2, I end up with this working example.
Create an app https://go.microsoft.com/fwlink/?linkid=2083908
Add permissions in {Your APP admin panel} > API permissions > Add permission > Microsoft Graph > Application permissions > Mail.Send > Add permissions
Create certificate to get client secret {Your APP admin panel} > Certificates & secrets > Client secrets > New client secret (Save the "Value" string somewhere this is your client_secret)
Make sure you have installed required node apps
Now you can run this code to send any email from your server:
const msal = require('#azure/msal-node');
const fetch = require('node-fetch');
const clientSecret = process.env.CLIENT_SECRET;
const clientId = process.env.CLIENT_ID;
const tenantId = process.env.TENANT_ID;
const aadEndpoint =
process.env.AAD_ENDPOINT || 'https://login.microsoftonline.com';
const graphEndpoint =
process.env.GRAPH_ENDPOINT || 'https://graph.microsoft.com';
const msalConfig = {
auth: {
clientId,
clientSecret,
authority: aadEndpoint + '/' + tenantId,
},
};
const tokenRequest = {
scopes: [graphEndpoint + '/.default'],
};
const cca = new msal.ConfidentialClientApplication(msalConfig);
const tokenInfo = await cca.acquireTokenByClientCredential(tokenRequest);
const mail = {
subject: 'Microsoft Graph JavaScript Sample',
//This "from" is optional if you want to send from group email. For this you need to give permissions in that group to send emails from it.
from: {
emailAddress: {
address: 'noreply#company.com',
},
},
toRecipients: [
{
emailAddress: {
address: 'someemail#domain.com',
},
},
],
body: {
content:
'<h1>MicrosoftGraph JavaScript Sample</h1>This is the email body',
contentType: 'html',
},
};
const headers = new fetch.Headers();
const bearer = `Bearer ${tokenInfo.accessToken}`;
headers.append('Authorization', bearer);
headers.append('Content-Type', 'application/json');
const options = {
method: 'POST',
headers,
body: JSON.stringify({ message: mail, saveToSentItems: false }),
};
await fetch(
graphEndpoint + '/v1.0/users/youroutlookemail#company.com/sendMail',
options
);
Also take a look here for email setting:
https://learn.microsoft.com/en-us/graph/api/user-sendmail?view=graph-rest-1.0&tabs=javascript
May be it will help someone ;)
I'd suggest that you use Microsoft Graph to send emails. There's an easy to use REST API that works greatly with OAuth.
Please find below some links to help you build this out quickly.
https://learn.microsoft.com/en-us/graph/api/user-sendmail?view=graph-rest-1.0&tabs=http
https://learn.microsoft.com/en-us/graph/auth/auth-concepts?view=graph-rest-1.0
https://learn.microsoft.com/en-us/graph/tutorials/node?view=graph-rest-1.0
SMTP is being limited (for Office365)
Microsoft is moving to discourage direct SMTP protocol access (regardless whether auth is basic or modern), especially so for automated scripts/jobs. The proposed best practice (for Office365) is to stop using SMTP protocol and instead use the Microsoft Graph API to send emails.
NodeMailer (as of Oct-2022) has transports for SMTP, sendmail, SES - no support yet for Microsoft Graph transport. [Feature Request].
Implementation: Send Email (via Microsoft Graph-> sendMail)
Javascript/Node10+, Tested ~Nov 2022. Heavily sourced from Bogdan Le's answer
let tenantID = "" // Get from Azure App Registration
let oAuthClientID = "" // Get from Azure App Registration
let clientSecret = "" // Get from Azure App Registration
let oAuthToken; // declared, gets defined if successfully fetched
let userFrom = "foo#bar.com"
let msgPayload = {
//Ref: https://learn.microsoft.com/en-us/graph/api/resources/message#properties
message: {
subject: 'Test',
body: {
contentType: 'HTML',
content: 'Test123'
},
toRecipients: [{emailAddress: {address: 'meganb#contoso.onmicrosoft.com'}}]
}
};
const axios = require('axios'); //using axios as http helper
await axios({ // Get OAuth token to connect as OAuth client
method: 'post',
url: `https://login.microsoftonline.com/${tenantID}/oauth2/token`,
data: new URLSearchParams({
client_id: oAuthClientID,
client_secret: clientSecret,
resource: "https://graph.microsoft.com",
grant_type: "client_credentials"
}).toString()
})
.then(r => oAuthToken = r.data.access_token)
await axios ({ // Send Email using Microsoft Graph
method: 'post',
url: `https://graph.microsoft.com/v1.0/users/${userFrom}/sendMail`,
headers: {
'Authorization': "Bearer " + oAuthToken,
'Content-Type': 'application/json'
},
data: msgPayload
})
Appendix 1: Create Azure oAuth App for sending emails
Create an Azure App Registration
Add mail sending permission: Azure App Registration Admin > API permissions > Add permission > Microsoft Graph > Application permissions > Mail.Send
WARNING: You will want to limit access of the app registration to specific mailboxes using application access policy. By default
its tenant wide - allows impersonation of any user's mailbox!
Grant Admin Consent to permission to your newly created Mail.Send permission (likely requires Global Admin)
Create Application Password: Azure App Registration Admin > Certificates and secrets > Client Secrets -> New client secret (note the generated secret)
Create a reminder/plan to refresh this secret every expiration period (in Azure as well as your own code)
Appendix 2: Limit Mail.Send permission
Office 365 Admin: Create a mail-enabled security group (note the group name)
Add Members to that group: Add user(s) whose mailbox you want your App Registration to be able impersonate via Mail.Send.
Create (and test) Application Access Policy, via Powershell:
##Variables - Manually define these:
$AppClientID="" #ClientID from Azure App Registration
$Group="" #Name of Office365 group
$ForbiddenMailbox="" #Address of mailbox AppRegistration should NOT have permission to
$AllowedMailbox="" #Address of mailbox AppRegistration should have permission to
##Connect to Exchange
Install-Module -Name ExchangeOnlineManagement
Connect-ExchangeOnline
##Create ApplicationAccessPolicy
New-ApplicationAccessPolicy -AppId $AppClientID -PolicyScopeGroupId $Group -AccessRight RestrictAccess -Description "Limit AppRegistration mailbox permissions to subset of users"
##Test ApplicationAccessPolicy - Denied Scenario
Test-ApplicationAccessPolicy -Identity $ForbiddenMailbox -AppId $AppClientID
##Test ApplicationAccessPolicy - Granted Scenario
Test-ApplicationAccessPolicy -Identity $AllowedMailbox -AppId $AppClientID

Is there a way to programatically login to using AzureAD with Cypress on PKCE flow?

I want to athenticate myself (React application) using cypress.js (https://www.cypress.io/). Is there a way to do it programatically with PKCE? As i was reading and looking into all examples - all of them are using implicit flow
I was trying to use solutions like https://www.npmjs.com/package/react-adal but with no success as it needs an implicit flow to be turned on
I was trying this as well: https://xebia.com/blog/how-to-use-azure-ad-single-sign-on-with-cypress/ with no success
I expected to programatically signin inside cypress and save user info and access_token to sessionStorage to be able to perform another api calls
I have not found a way to do this programmatically with PKCE per se, but with the MSAL 2.0 library (#azure/msal-browser on npm) I was able to fill in the account cache ahead of time so it thought it was already logged in. The process looks like this:
With a cy.task, send a request to Azure AD using the ROPC flow to get the tokens.
const scopes = [
'openid',
'profile',
'user.read',
'email',
'offline_access' // needed to get a refresh token
];
const formdata = new URLSearchParams({
'grant_type': 'password',
'scope': scopes.join(' '),
'client_info': 1, // returns an extra token that MSAL needs
'client_id': aadClientId,
'client_secret': aadClientSecret,
'username': aadUsername,
'password': aadPassword,
});
const response = await fetch(`https://login.microsoft.com/${aadTenantId}/oauth2/v2.0/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: formdata.toString(),
});
const tokens = await response.json();
Transform the tokens into the cache entries that MSAL wants (based on observing it in a real browser)
// The token tells us how many seconds until expiration;
// MSAL wants to know the timestamp of expiration.
const cachedAt = Math.round(new Date().getTime()/1000);
const expiresOn = cachedAt + tokens.expires_in;
const extendedExpiresOn = cachedAt + tokens.ext_expires_in;
// We can pull the rest of the data we need off of the ID token body
const id_token = JSON.parse(Buffer.from(tokens.id_token.split('.')[1], 'base64').toString('utf-8'));
const clientId = id_token.aud;
const tenantId = id_token.tid;
const userId = id_token.oid;
const name = id_token.name;
const username = id_token.preferred_username;
const environment = 'login.windows.net'; // 🤷‍♂️
const homeAccountId = `${userId}.${tenantId}`;
const cacheEntries = {};
// client info
cacheEntries[`${homeAccountId}-${environment}-${tenantId}`] = JSON.stringify({
authorityType: 'MSSTS',
clientInfo: tokens.client_info,
environment,
homeAccountId,
localAccountId: userId,
name,
realm: tenantId,
username,
});
// access token
cacheEntries[`${homeAccountId}-${environment}-accesstoken-${clientId}-${tenantId}-${token.scope}`] = JSON.stringify({
cachedAt: cachedAt.toString(),
clientId,
credentialType: "AccessToken",
environment,
expiresOn: expiresOn.toString(),
extendedExpiresOn: extendedExpiresOn.toString(),
homeAccountId,
realm: tenantId,
secret: tokens.access_token,
target: tokens.scope,
});
// id token
cacheEntries[`${homeAccountId}-${environment}-idtoken-${clientId}-${tenantId}-`] = JSON.stringify({
clientId,
credentialType: "IdToken",
environment,
homeAccountId,
realm: tenantId,
secret: tokens.id_token,
});
// refresh token
cacheEntries[`${homeAccountId}-${environment}-refreshtoken-${clientId}--`] = JSON.stringify({
clientId,
credentialType: "RefreshToken",
environment,
homeAccountId,
secret: tokens.refresh_token,
});
Use cy.window to store those in sessionStorage or localStorage, depending on how you have MSAL configured.
cy.task('login').then(cacheEntries => {
cy.window().then(window => {
for (let entry in cacheEntries) {
window.sessionStorage.setItem(entry, cacheEntries[entry]);
}
});
});
It's super fragile and not very pretty, but it works! The user that Cypress logs in as needs MFA to be disabled, of course.

Why do I get a 404 response when I try to post to github API whilst authenticated with oauth access tokens?

I am using the passportjs library to authenticate users into the application. An access token is usually generated when users authenticate successfully with passportjs. I am attempting to create a branch with the github API with this access token but without much success, both using the octokit wrapper as well as posting with super-agent.
I first attempted to authenticate the octokit by providing it username and password, in this fashion.
let octokit=new Octokit({
auth:{
username:username,
password:password
}
});
I was then able to create a ref/branch without much issue. However, when I did the same but with accesstokens generated by github SSO, like this
passport.use(new GitHubStrategy({
clientID: keys.clientId,
clientSecret: keys.clientSecret,
callbackURL: "/auth/github/callback"
},
async (accessToken, refreshToken, profile, done) => {
let octokit = new Octokit(auth: `token ${accessToken}`);
const branchName = 'refs/heads/vue' + Math.random();
let masterHash = '123x123x1231232';
octokit.git.createRef({
owner: owner,
repo: 'gitDemo',
ref: branchName,
sha: masterHash
}).then((data) => {
console.log('data ', data);
});
}
I receive an HttpError: Not found error. Another method that I tried is to post directly to the end point with superagent, putting the acccess code in the authorization header.
const data={
ref:'refs/heads/FooBranch',
sha:masterHash
};
const res2=await request.post('https://api.github.com/repos/SomeOwner/SomeRepo/git/refs')
.set('Authorization','token '+accessToken)
.send(data);
However, I still receive an HttpError :not found issue. I am quite confused as to what I may have done wrong. Thank you and any help would be greatly appreciated!
I found the anwser here
Basically you don't send data using JSON but rather FormData.
So the post should look like this (copied from link):
let data = new FormData()
data.append('client_id', options.client_id)
data.append('client_secret', options.client_secret)
data.append('code', code)
fetch(`https://github.com/login/oauth/access_token`, {
method: 'POST',
body: data
})
In case anyone else comes across this in the future, you have to specify the Content-Type and Accept when making the request. Without specifying it in the headers you will have to send and receive FormData.
Kudos #Github for not mentioning this at all in their docs.
Using node's built in fetch:
const githubRes = await fetch(githubUrl, {
method: "POST",
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
code,
client_id: config.github.clientId,
client_secret: config.github.clientSecret,
redirect_uri: config.github.redirectUri,
}),
});
const githubJson = await githubRes.json();
const token = githubJson.access_token;

Get access_token from passport-azure-ad.OIDCStrategy for use as bearer token

** Disclaimer -- I'm new to the world of oAuth and OpenIDConnect -- please be patient if I'm asking a stupid question here.
I want to create a SPA that will request data from an API. Both the SPA and API are hosted on the same nodejs server. I want anyone accessing data and/or the app to be authenticated with our AzureAD tenant on Office365.
Currently, I have the authentication piece working using passport-azure-ad.OIDCStrategy. However, in my app, I would also like to be able to access information from the Microsoft GRAPH api in the server side api code. However, the OIDC connection that I've already made does not seem to be enough to allow me access to the GRAPH api. It appears that maybe I need a jwt bearer token.
My question is, do I need to use the access token from the OIDC response to get a bearer token? If so, how do I go about this (on the server side -- nodejs)?
I tried viewing the example listed in passport-auth-ad for BearerStrategy v2 endpoint. What confuses me though is that it uses OIDCStrategy! Does that also return a bearer token? If so, am I already receiving everything I need in my first OIDCStrategy call?
Thanks for whatever help you can offer!
Update
https.request({
hostname: "graph.microsoft.com",
path: '/v1.0/me/messages',
port: 443,
method: 'GET',
headers: {Authorization: 'Bearer ' + req.user.token, Accept: "application/json"}
},(rs) => {
console.log("HTTPS Response Status: ", rs.statusCode);
console.log("HTTPS Response Headers: ", rs.headers)
rs.on('data', (d) => {
res.send(d)
})
}).end();
Error Message:
{
"error": {
"code": "InvalidAuthenticationToken",
"message": "Access token validation failure.", ...
I confirmed that the token is the same token that was passed as the id_token in the auth callback from Azure. Any thoughts?
Update 2
A few more code snippets to help in diagnosing where I may be going wrong.
Strategy Config
//Still test code so user management not fully implemented
passport.use("azure", new azureStrategy({
identityMetadata: 'https://login.microsoftonline.com/common/.well-known/openid-configuration',
clientID: "*********************",
responseType: 'code id_token',
issuer: "https://sts.windows.net/****************/",
responseMode: 'form_post',
redirectUrl: "https://localhost:5070/auth/azure/callback",
allowHttpForRedirectUrl: true,
clientSecret: "***************" ,
state: "************"
},
(iss, sub, profile, claims, accessToken, refreshToken, params, done) => {
process.nextTick(() => {
var user = usvc.findUserByAltId(profile.oid, "azure");
if(!user){
}
})
done(null, {id: profile.oid, name: profile.displayName, email: profile.upn, photoURL: "", token: params.id_token });
}));
Route Definitions
app.get("/auth/azure", azure.passport.authenticate(
'azure', {scope: ['Mail.Read','User.Read'], failureRedirect: '/'}))
app.post("/auth/azure/callback", azure.passport.authenticate(
"azure", {scope: ['Mail.Read','User.Read'], failureRedirect: "/error.html"}),
(req, res) => {res.redirect("/user")})
The OpenIDConnect work grand flow also will returns a JWT token for Authentication & Authorization. You can use the id_token in Authentication header for the resources. However, some operations in Graph APIs require an administrator permission.
You can try to run the following script in PowerShell to upgrade your Azure AD application's privilege.
Connect-MsolService
$ClientIdWebApp = '{your_AD_application_client_id}'
$webApp = Get-MsolServicePrincipal –AppPrincipalId $ClientIdWebApp
#use Add-MsolRoleMember to add it to "Company Administrator" role).
Add-MsolRoleMember -RoleName "Company Administrator" -RoleMemberType ServicePrincipal -RoleMemberObjectId $webApp.ObjectId

Resources