I'm struggling to solve an issue that I got on trying to login via Single Sign On from Microsoft Azure using Cypress. It is possible to do it without using the Client_Secret? How can I do it?
I've been spending more than a week trying to solve this situation...
I'm a junior on this, so if you could help-me I would be very grateful.
Thanks a lot,
Yes, you can. Navigate to your AD App in the portal -> Authentication -> set Allow public client flows to Yes like below.
Then in the blog, in step Cypress utility for mimicking react-adal, it uses the client credential flow, there is a comment post by #Bryce Kolton under the blog, he uses the ROPC flow, in this flow, you could use it without Client_Secret via a public client App as you changed above(Allow public client flows), just refer to it.
/* eslint-disable no-underscore-dangle */
import { AuthenticationContext } from ‘react-adal’;
import { azTenantId, azClientId } from ‘../../server/config/environment’;
// Need to get data points from server’s environment, not src
const adalConfig = {
tenant: azTenantId,
clientId: azClientId,
cacheLocation: ‘localStorage’,
replyUrl: ‘/’,
endpoints: {
api: ”,
},
};
const authContext = new AuthenticationContext(adalConfig);
export default async function doLogin() {
// getCachedToken also does an expiration check so we know for sure the tokens are usable
if (
!authContext.getCachedToken(adalConfig.endpoints.api)
|| !authContext.getCachedToken(adalConfig.clientId)
) {
const response = await cy.request({
method: ‘POST’,
url:
‘https://login.microsoftonline.com/mercedesme.onmicrosoft.com/oauth2/token’,
// qs: { ‘api-version’: ‘1.0’ }, // uncomment if your consuming resource expects the ‘aud’ to have a prefix of ‘sn:’
headers: {
‘cache-control’: ‘no-cache’,
‘content-type’:
‘multipart/form-data; boundary=—-WebKitFormBoundary7MA4YWxkTrZu0gW’,
},
form: true,
body: {
grant_type: ‘password’,
response_type: ‘code’,
client_id: ‘[[yourappsclientid]]’,
username: ‘[[yourtestuzseremail]]’,
password: ‘[[yourtestuserpassword]]!’,
scope: ‘openid’,
resource: ‘[[some-resource-id]]’,
},
});
// Store the token and data in the location where adal expects it
authContext._saveItem(authContext.CONSTANTS.STORAGE.IDTOKEN, response.body.access_token);
authContext._saveItem(
authContext.CONSTANTS.STORAGE.ACCESS_TOKEN_KEY + adalConfig.endpoints.api,
response.body.access_token,
);
authContext._saveItem(
authContext.CONSTANTS.STORAGE.ACCESS_TOKEN_KEY + adalConfig.clientId,
response.body.access_token,
);
authContext._saveItem(
authContext.CONSTANTS.STORAGE.EXPIRATION_KEY + adalConfig.endpoints.api,
response.body.expires_on,
);
authContext._saveItem(
authContext.CONSTANTS.STORAGE.EXPIRATION_KEY + adalConfig.clientId,
response.body.expires_on,
);
authContext._saveItem(
authContext.CONSTANTS.STORAGE.TOKEN_KEYS,
[adalConfig.clientId].join(authContext.CONSTANTS.RESOURCE_DELIMETER)
+ authContext.CONSTANTS.RESOURCE_DELIMETER,
);
}
}
To use the ROPC flow successfully, make sure your scenario meets the requirements below, e.g. your user account is not MAF-enabled.
Related
I am using node-ews to fetch emails from the Microsoft Exchange server.
It was working fine with basic auth.
But, as Microsoft disabled basic auth.
We are currently using the OAuth token (access token) from Graph Explorer to test.
But it's returning 401 Unauthorised error.
This is the sample code we are using to connect to the exchange server.
const ewsConfig = {
username: item.mail_username,
password: item.user_pass,
host: item.ews_host,
token: 'xxxxxxxxxxx',
auth: 'bearer'
};
// initialize node-ews
const options = {
rejectUnauthorized: false,
strictSSL: false
};
// initialize node-ews
const ews = new EWS(ewsConfig, options);
. We are currently using the OAuth token (access token) from Graph Explorer to test.
The Graph Explorer token won't have permissions for EWS only Graph, the only two permission that are valid in EWS are EWS.AccessAsUser.All or full_access_as_app if using the client credentials flow. https://learn.microsoft.com/en-us/exchange/client-developer/exchange-web-services/how-to-authenticate-an-ews-application-by-using-oauth the Mail.Read etc permission don't work in EWS because it doesn't support the more restrictive authentication scheme that Graph supports (which is a reason to use the Graph over EWS)
If you want to accesstoken to test with use the EWSEditor https://github.com/dseph/EwsEditor/releases and grab its token
Part 1-1 - Setup application in AZURE that allows to generate MSAL-access token for EWS:
Login to MS AZURE portal.
Open "App registration" tool:
step2_img
Click "New Registration":
step3_img
Setup new App:
step4_img
After you click registrate button you will receive smtg like this:
step5_img
Open API permissions tab for previously created App + click Add permission and select MS Graph:
step6_img
Select Delegated permissions:
step7_img
Find User section and select User.Read + Add permission click:
step8_img
Add a permission again + APIs my organizaton uses tab(or find it) and find Office 365 Exchange Online:
step9_img
Part-1-2 - continue...
Part 2 - get accessToken by using userName + userPassword to email box:
import * as path from 'path';
import { ExchangeService, EmailMessage, MessageBody, OAuthCredentials, AutodiscoverService, Folder, Item, ExchangeVersion } from 'ews-javascript-api';
public async getEmailAccessToken(
clientId: string,
tenantId: string,
emailUserName: string,
emailUserPassword: string,
cacheFilePath: string = `.${path.sep}tokenCache.json`) {
const msal = require('#azure/msal-node');
const { promises: fs } = require('fs');
//Cache Plugin configuration
const beforeCacheAccess = async (cacheContext) => {
try {
const cacheFile = await fs.readFile(cacheFilePath, 'utf-8');
cacheContext.tokenCache.deserialize(cacheFile);
} catch (error) {
// if cache file doesn't exists, create it
cacheContext.tokenCache.deserialize(await fs.writeFile(cacheFilePath, ''));
}
};
const afterCacheAccess = async (cacheContext) => {
if (cacheContext.cacheHasChanged) {
try {
await fs.writeFile(cacheFilePath, cacheContext.tokenCache.serialize());
} catch (error) {
console.log(error);
}
}
};
const cachePlugin = {
beforeCacheAccess,
afterCacheAccess
};
const msalConfig = {
auth: {
clientId: clientId, // YOUR clientId
authority: `https://login.microsoftonline.com/${tenantId}` // YOUR tenantId
},
cache: {
cachePlugin
},
system: {
loggerOptions: {
loggerCallback(loglevel, message, containsPii) {
console.log(message);
},
piiLoggingEnabled: false,
logLevel: msal.LogLevel.Verbose
}
}
};
const pca = new msal.PublicClientApplication(msalConfig);
const msalTokenCache = pca.getTokenCache();
const accounts = await msalTokenCache.getAllAccounts();
// Acquire Token Silently if an account is present
let accessToken = null;
if (accounts.length > 0) {
const silentRequest = {
account: accounts[0], // Index must match the account that is trying to acquire token silently
scopes: ['https://outlook.office365.com/EWS.AccessAsUser.All'],
};
const response = await pca.acquireTokenSilent(silentRequest);
accessToken = response.accessToken;
} else {
// fall back to username password if there is no account
const usernamePasswordRequest = {
scopes: ['https://outlook.office365.com/EWS.AccessAsUser.All'],
username: emailUserName, // Add your username here
password: emailUserPassword, // Add your password here
};
const response = await pca.acquireTokenByUsernamePassword(usernamePasswordRequest);
accessToken = response.accessToken;
}
return accessToken;
}
This method returns accessToken allows us to use EWS-api and also generates tokenCacheFile.json that will be used for silent usage in case of multiple calls.
Part 3 - connect to emailbox by using previously generated accessToken and ews-javascript-api :
import { ExchangeService, EmailMessage, MessageBody, OAuthCredentials, AutodiscoverService, Folder, Item, ExchangeVersion } from 'ews-javascript-api';
public async connectAndChangeAllEmailsFromBlaBla(
clientId: string,
tenantId: string,
exchangeServiceUrl: string = 'https://outlook.office365.com/Ews/Exchange.asmx',
emailUserName: string,
emailUserPassword: string,
searchMask: string = 'hasattachments:yes and from:NoReply#blabla.com and received:today') {
// get acces token by method written above in part 2
const emailAccessToken = await this.getEmailAccessToken(clientId, tenantId, emailUserName, emailUserPassword);
const ews = require('ews-javascript-api');
const service = new ExchangeService(ews.ExchangeVersion.Exchange2013);
// use emailAccesToken
service.Credentials = new OAuthCredentials(emailAccessToken);
service.Url = new ews.Uri(exchangeServiceUrl);
const mailInbox = await ews.Folder.Bind(service, ews.WellKnownFolderName.Inbox);
const loadPageSize = 1000; // 1 means load last email according to filter
const view = new ews.ItemView(loadPageSize);
view.PropertySet = new ews.PropertySet(ews.BasePropertySet.FirstClassProperties);
let mailItems;
// hasattachment:yes
// isread:false
// received:today or received:[date]
mailItems = await mailInbox.FindItems(searchMask, view);
console.log(`Emails were found before processing: ${mailItems.Items.length}`);
for (const item of mailItems.Items) {
// mark mail.item as read
item.IsRead = true;
await item.Update(1);
// Do what you want
}
return mailItems.Items.length;
}
Part 0 - Please find the solution we used to fix the same problem.
The solution consist of 3 parts:
Setup application in AZURE that allows to generate MSAL-access token for EWS.
Add code to get accessToken.
Made changes in old code to use previously received accessToken. I am usind ews-javascript-api. But I think previouse two steps will help you to get accessToken for EWS and you can use it with node-EWS.
Sorry for 3 posts, but as a new user I have a restrictions it impossible for new users to create posts with more than 8 links and etc... )
Part 1-2 - continue:
Find EWS section and select EWS.AccessAsUser.All and click Add permissons:
step10_img
Go to Authentication tab and click Add platform:
step11_img
Select Mobile and Desctop apps and click Save button:
step12_img
Select two options and click Configure:
step13-1_img
step13-2_img
Also on Authentication tab set "Supported accounts types" and "Allow public client flows" and click Save:
step14_img
Go to Overview tab you should see smthg like this:
clientID
tenantId
step15_img
THIS STEP should be made BY EACH USER that WILL USE this API - use USER credentials to open this link (or YOUR ADMIN should make bulk apply). Check made changes by opening next link in browser in incognito mode(FOR each user):
https://login.microsoftonline.com/ADD YOUR TENANTID/oauth2/v2.0/authorize?
client_id=ADD YOUR CLIENTID
&response_type=code
&redirect_uri=https://login.microsoftonline.com/common/oauth2/nativeclient
&response_mode=query
&scope=EWS.AccessAsUser.All
&state=12345
After the opening previously generated link you should login and then receive another link in browser which shoud contains generated code:
step16_img
Now we can start add code allows us to get accessToken
Edit:
The problem was a simple typo in the Header. You're probably wasting your time here
In essence, I have the same problem as described here. It's a somewhat different usecase and I'll try to provide as much context as I can in the hopes that someone will be able to solve the problem.
So, this has to do with Azure, which seems to be an Alias for "Crazy problem generator". My apologies.
I'm trying to write a Service in NodeJS which has the purpose of synchronizing another app's database with data from Azure.
For that reason, I'm using msal-node's Client Credential Flow as described here.
I find their comment // replace with your resource quite ridiculous, as I have not found a single full example online that specifies the format that should be used.
Intuitively, I would use something like
['GroupMember.Read.All']
//or
['https://graph.microsoft.com/GroupMember.Read.All']
Unfortunately, this does not work. Luckily, I get an error that describes the problem (even if only when this is the only scope I use, othewise the error is garbage):
{
// ...
errorMessage: '1002012 - [2022-05-23 11:39:00Z]: AADSTS1002012: The provided value for scope https://graph.microsoft.com/bla openid profile offline_access is not valid. Client credential flows must have a scope value with /.default suffixed to the resource identifier (application ID URI).\r\n'
}
Okay, let's do that:
['https://graph.microsoft.com/GroupMember.Read.All/.default']
Now, the app actually performs a request, but unfortunately, I get
{
// ...
errorCode: 'invalid_resource',
errorMessage: '500011 - [2022-05-23 11:42:31Z]: AADSTS500011: The resource principal named https://graph.microsoft.com/GroupMember.Read.All was not found in the tenant named <My company name, not an ID as shown in some places>. This can happen if the application has not
been installed by the administrator of the tenant or consented to by any user in the tenant. You might have sent your authentication request to the wrong tenant.\r\n' +
'Trace ID: <some id>\r\n' +
'Correlation ID: <some id>\r\n' +
'Timestamp: 2022-05-23 11:42:31Z - Correlation ID: <some id> - Trace ID: <some id>',
}
And yet it is there
And I am able to get a token for the .default scope. That's just not good for anything.
The important parts of the actual code:
import fetch from 'isomorphic-fetch';
import * as msal from '#azure/msal-node';
// got env variables using dotenv package
// this is Typescript
const msalConfig = {
auth: {
clientId: process.env.OAUTH_APP_ID!,
authority: process.env.OAUTH_AUTHORITY!,
clientSecret: process.env.OAUTH_APP_SECRET!
},
system: {
loggerOptions: {
loggerCallback(loglevel: any, message: any, containsPii: any) {
console.log(message);
},
piiLoggingEnabled: false,
logLevel: msal.LogLevel.Verbose,
}
}
};
const msalClient = new msal.ConfidentialClientApplication(msalConfig);
const allCompanyMembersGroupId = '<some id>';
const tokenRequest = {
scopes: ['https://graph.microsoft.com/GroupMember.Read.All/.default']
};
msalClient.acquireTokenByClientCredential(tokenRequest).then(response => {
console.log('Got token:', response);
fetch(`https://graph.microsoft.com/v1.0/groups/${allCompanyMembersGroupId}/members`, {
method: 'GET',
headers: {
Authority: `Bearer ${response!.accessToken}`
}
}).then((res: any) => {
console.log('Got response:', res);
})
});
As mentioned, the request isn't performed with my GroupMember.Read.All scope. With the default scope, I get a 401 unauthorized error.
So, these are my questions:
How to fix this?
Okay, if it you don't know how to fix it, what is the exact format required for the scope? Is the prefix https://graph.microsoft.com correct, even for my specific app?
Is this the correct library to use, or is this just broken code or not intended for such use? The other question I linked to above mentions that requests were successful using Postman, just not this lib...
Thanks heaps for any advice!
It's a problem with the headers. It should be Authorization instead of Authority.
The error is in the following variable:
const tokenRequest = {
scopes: ['https://graph.microsoft.com/GroupMember.Read.All/.default']
};
the correct one would be:
const tokenRequest = {
scopes: ['https://graph.microsoft.com/.default']
};
Note: When using Client Credentials flow, it is a must to specify the scope string tailing with "/.default".
For eg:
If you are trying to get an access-token using client credentials
flow for MS Graph API, the scope should be
"https://graph.microsoft.com/.default".
If you are trying to get an
access-token using client credentials flow for a custom API which is
registered in AAD, the scope should be either
"api://{API-Domain-Name}/.default" or
"api://{API-Client-Id}/.default".
These are my notes for how to UI test an Azure AD single page app using MSAL.js and ADFS (in our case on-premise) and the schema associated with the process of token creation and local storage.
From the tutorial: "It uses the ROPC authentication flow to acquire tokens for a test user account, and injects them into browser local storage before running the tests. This way MSAL.js does not attempt to acquire tokens as it already has them in cache."
After watching the awesome video here:
https://www.youtube.com/watch?v=OZh5RmCztrU
...and going through the repo here:
https://github.com/juunas11/AzureAdUiTestAutomation
I was stuck trying to match my use of on-premise ADFS with MSAL.js 2.0 and session store, with that of the above tutorial and code. So if you are using the link to Azure ending with /adfs/oauth2/token ( as opposed to oAuth /oauth2/v2.0/token ) - then follow the below!!
MOST of the changes I made were from auth.js: https://github.com/juunas11/AzureAdUiTestAutomation/blob/main/UiTestAutomation.Cypress/cypress/support/auth.js
Simply follow the tutorial and copy in that content, then change the following:
const environment = ''; (mine was corporate domain NOT login.windows.net)
for the Account entity (const buildAccountEntity) use:
authorityType: 'ADFS',
...and REMOVE the line: clientInfo: "",
for the Access Token entity: (const buildAccessTokenEntity):
...ADD the line: tokenType: 'bearer',
ADD a new function for the Refresh Token (new) entity:
const buildRefreshTokenEntity = (homeAccountId: string, accessToken: string) => {
return {
clientId,
credentialType: 'RefreshToken',
environment,
homeAccountId,
secret: accessToken,
};
};
next I had to MATCH my sessionStorage TOKEN by running it locally using VS Code and logging in then reverse-engineering the required KEY-VALUE pairs for what was stored (results are in next code block!).
Specifically I kept case-sensitivity for 'home account', I blanked-out some values, and had to add in the RefreshToken part, and mine used Session Storage (not local storage), and match the extended expires with the same value (based on my sample run through only):
const injectTokens = (tokenResponse: any) => {
const scopes = ['profile', 'openid'];
const idToken: JwtPayload = decode(tokenResponse.id_token) as JwtPayload;
const localAccountId = idToken.sub; // in /oauth2/v2.0/token this would be: idToken.oid || idToken.sid; however we are using /adfs/oauth2/token
const realm = ''; // in /oauth2/v2.0/token this would be: idToken.tid; however we are using /adfs/oauth2/token
const homeAccountId = `${localAccountId}`; // .${realm}`;
const homeAccountIdLowerCase = `${localAccountId}`.toLowerCase(); // .${realm}`;
const usernameFromToken = idToken.upn; // in /oauth2/v2.0/token this would be: idToken.preferred_username; however we are using /adfs/oauth2/token
const name = ''; // in /oauth2/v2.0/token this would be: idToken.name; however we are using /adfs/oauth2/token
const idTokenClaims = JSON.stringify(idToken);
const accountKey = `${homeAccountIdLowerCase}-${environment}-${realm}`;
const accountEntity = buildAccountEntity(homeAccountId, realm, localAccountId, idTokenClaims, usernameFromToken, name);
const idTokenKey = `${homeAccountIdLowerCase}-${environment}-idtoken-${clientId}-${realm}-`;
const idTokenEntity = buildIdTokenEntity(homeAccountId, tokenResponse.id_token, realm);
const accessTokenKey = `${homeAccountIdLowerCase}-${environment}-accesstoken-${clientId}-${realm}-${scopes.join(' ')}`;
const accessTokenEntity = buildAccessTokenEntity(
homeAccountId,
tokenResponse.access_token,
tokenResponse.expires_in,
tokenResponse.expires_in, // ext_expires_in,
realm,
scopes,
);
const refreshTokenKey = `${homeAccountIdLowerCase}-${environment}-refreshtoken-${clientId}-${realm}`;
const refreshTokenEntity = buildRefreshTokenEntity(homeAccountId, tokenResponse.access_token);
// localStorage was not working, needs to be in sessionStorage
sessionStorage.setItem(accountKey, JSON.stringify(accountEntity));
sessionStorage.setItem(idTokenKey, JSON.stringify(idTokenEntity));
sessionStorage.setItem(accessTokenKey, JSON.stringify(accessTokenEntity));
sessionStorage.setItem(refreshTokenKey, JSON.stringify(refreshTokenEntity));
};
Lastly, in the login function I used the /adfs link as we use on-premise ADFS and MSAL.js v2.0 and did NOT need that client_secret:
export const login = (cachedTokenResponse: any) => {
let tokenResponse: any = null;
let chainable: Cypress.Chainable = cy.visit('/'); // need to visit root to be able to store Storage against this site
if (!cachedTokenResponse) {
chainable = chainable.request({
url: authority + '/adfs/oauth2/token', // was this '/oauth2/v2.0/token',
method: 'POST',
body: {
grant_type: 'password',
client_id: clientId,
// client_secret: clientSecret,
scope: ['profile openid'].concat(apiScopes).join(' '),
username,
password,
},
form: true,
});
***... MORE CODE OMITTED***
finally I ran using VSCode terminal 1 (yarn start) then terminal 2 (yarn run cypress open)
TYPESCRIPT use:
rename all files from .js to .ts
update tsconfig to include the cypress type on this line:
"types": ["node", "cypress"],
Now when I run Cypress I can navigate around my site and I am authenticated!! Hope this helped you save an hour or two!!
I am building an app in react-native and need to authenticate to a server via Azure AD. I have tried to google this but surprisingly there is not much content related to this.
I have found this library 'react-native-azure-ad' but not so much documentation about it. I dont understand how to use it.
My main questions is: Where do I need to place my server's url when using this library, in order to authenticate to it?
Thanks for any suggestions!
EDIT:
Here is my code:
import { ReactNativeAD, ADLoginView } from 'react-native-azure-ad'
const CLIENT_ID = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
class LandingView2 extends React.Component {
constructor(props) {
super(props)
this.AzureADContext = {
client_id: CLIENT_ID,
// Optional
redirect_url: 'http://localhost:8080',
// Optional
authority_host: 'xxxx',
// Optional
tenant: 'common',
// Optional
prompt: 'none',
// Optional
login_hint: 'user#domain.com',
// This is required if client_id is a web application id
// but not recommended doing this way.
client_secret: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
resources: [
'https://graph.microsoft.com',
'https://outlook.office365.com',
// ... more resources
]
}
}
render() {
new ReactNativeAD({
client_id: CLIENT_ID,
resources: [
'https://outlook.office365.com'
]
})
return <ADLoginView
context={ReactNativeAD.getContext(CLIENT_ID)}
onSuccess={this.onLoginSuccess.bind(this)} />
}
onLoginSuccess(credentials) {
console.log(credentials['https://outlook.office365.com'].access_token)
// use the access token ..
}
}
You need to create two applications in AAD.
a native application for your app
another application for your server/API
You can then use the clientId or AppID URI of the API as the "resource" for the code shown in the question. All the other parameters are taken from your client app.
If everything is successful, you will get an access token which you can then use as a bearer token when calling your API.
credentials['https://yourtenant.onmicrosoft.com/yourapiname'].access_token
I am trying to develop a node application that would be able to access my Outlook.com mails.
I am trying to do it in a way it doens't require me to enter my credentials, the application will know them (user name and password). I am not too worried about storing them in the config of my application.
I am using simple-oauth2 but I keep getting an error. The following is the code that is trying to retrieve the Oauth token:
const credentials = {
client: {
id: this.appId,
secret: this.appSecret,
},
auth: {
tokenHost: "https://login.microsoftonline.com",
authorizePath: "common/oauth2/v2.0/authorize",
tokenPath: "common/oauth2/v2.0/token",
},
};
const oathClient = oauth2.create(credentials);
const tokenConfig = {
username: "zzz#outlook.com",
password: "xxxxx",
scope: "Mail.Read",
};
const result = await oathClient.ownerPassword.getToken(tokenConfig);
const token = oathClient.accessToken.create(result);
However when calling get token I get the following response:
"error": "invalid_grant",
"error_description": "AADSTS70000: The grant is not supported by this API version\r\nTrace ID: 91935472-5d7b-4210-9a56-341fbda12a00\r\nCorrelation ID: 6b075f4e-b649-493e-a87b-c74f0e427b47\r\nTimestamp: 2017-08-19 14:00:33Z",
"error_codes": [ 70000],
I have aded the application in apps.dev.microsoft.com
Added a platform (Web API) for it.
Added the "Mail.Read" permission on Microsoft Grah
And I am using the apikey and secret I generated there.
Googling looks like all the examples I find are to connect is using a client certificate. Is it possible to use the API using the API credentials?
If the only way is using certificates, is there a way I can use simple-oauth2 for that?
Ok, looks like I was using the wrong method.
Trying to access using the ClientCredentials module on simple-ouath2 and its workign now:
const tokenConfig = {
scope: "https://graph.microsoft.com/.default",
};
const result = await oathClient.clientCredentials.getToken(tokenConfig);