ember-simple-auth-torii Facebook provider never returns promise from open function - ember-simple-auth

I'm using ember-simple-auth-torii with a custom Facebook OAuth2 authenticator, but I never seem to be able to have the promise return any data (for the data.authorizationCode). The popup window just hangs until I close it, at which point I get the popupClosed error message.
What am I missing that I should be doing?
Thanks!
FacebookAuthenticator = OAuth2.extend
torii: null
provider: "facebook-oauth2"
authenticate: (credentials) ->
that = this
new Ember.RSVP.Promise((resolve, reject) ->
that.torii.open(that.provider).then((data) ->
data =
facebook_auth_code: data.authorizationCode
that.makeRequest(that.serverTokenEndpoint, data).then ((response) ->
Ember.run ->
expiresAt = that.absolutizeExpirationTime(response.expires_in)
that.scheduleAccessTokenRefresh response.expires_in, expiresAt, response.refresh_token
resolve Ember.$.extend(response,
expires_at: expiresAt,
access_token: response.access_token,
user_id: response.user_id
)
), (xhr) ->
Ember.run ->
reject xhr.responseJSON or xhr.responseText
)
)
FacebookAuthentication =
name: "facebook-authentication"
before: "simple-auth"
after: 'torii'
initialize: (container) ->
Session.reopen
user: (->
userId = #get('user_id')
if (!Ember.isEmpty(userId))
return container.lookup('store:main').find('user', userId)
).property('userId')
torii = container.lookup('torii:main')
authenticator = FacebookAuthenticator.create
torii: torii
container.register("authenticator:facebook", authenticator, {
instantiate: false
})
`export default FacebookAuthentication`

The problem I was having was with an incorrect URL.
You need to set the URL in Facebook's advanced app settings, and ensure it is the same URL as your ember-cli app as specified in redirectUri.

Related

node-ews returning 401 Unauthorized where as using the valid access token

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

UI testing using Cypress with authentication to Azure AD using ADFS

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!!

Can I change other pieces of state in a Recoil Atom Effect

The Situation
I am using Recoil to manage state in a React application. I am using Atom Effects to back up recoil values with an API.
When my Atom detects something wrong with the authentication token I would like it to somehow convey that issue to the rest of the application so that the application can update state.
The Details
I have several Atoms:
An atom which stores an authentication token, for use when communicating with the storage API.
Atoms for various pieces of the application which I would like to save on that storage API.
In order to perform the API synchronization I have an apiStorageEffect Atom Effect. The effect uses a recoil-stored authentication token using getPromise and forwards changes using an onSet handler. That handler uses the token when calling the save method. The save method can detect when authentication has failed.
When authentication fails I need to trigger a log out event (setting the token to null, and resetting a series of atoms to their defaults).
Here is a minimal version of the storage effect:
const apiStorageEffect = (key) => async ({
onSet,
getPromise,
}) => {
const authToken = await getPromise(authTokenAtom)
if (!authToken) { return }
onSet(async (newValue) => {
const result = await saveToApiStorage(key, newValue, authToken)
if (result.failed) {
// INSERT CODE TO TRIGGER LOGOUT HERE
}
})
}
The Question
I can think of a few possible paths for triggering logout from within an effect, but all of them involve somehow sending information outside of the affected atom to the main application.
Is it possible to modify atoms OTHER than the atom / node in question from within an atom effect?
Is it possible for an atom effect to emit an event that can be immediately handled elsewhere in the React application?
Is it possible for an atom effect to invoke a method that can modify recoil state more broadly?
Atom effects are still pretty new (currently unstable).
The behavior that you desire would naturally be accomplished by subscribing to all of the related atoms via a selector, and processing the effect there... however, selector effects don't yet exist.
The most straightforward way to do this now is to convert your effect into a component which performs the operation using the effect hook (explained in the recoil docs). That component would then be placed underneath the RecoilRoot providing the recoil state. Here's an example based on the information in your question:
TS Playground
import {useCallback, useEffect} from 'react';
import {atom, RecoilRoot, useRecoilValue, useResetRecoilState} from 'recoil';
declare function saveToApiStorage (
key: string,
data: { value1: number; value2: number; },
authToken: string,
): Promise<{failed: boolean}>;
export const authTokenState = atom<string | null>({
key: 'authToken',
default: null,
});
// the key used in your effect
export const keyState = atom({key: 'key', default: ''});
// some bit of state for the API
export const value1State = atom({key: 'value1', default: 0});
// another one
export const value2State = atom({key: 'value2', default: 0});
// this is the converted effect component
export function ApiStorageEffect (): null {
const key = useRecoilValue(keyState);
const authToken = useRecoilValue(authTokenState);
const value1 = useRecoilValue(value1State);
const value2 = useRecoilValue(value2State);
const resetAuthToken = useResetRecoilState(authTokenState);
const resetValue1 = useResetRecoilState(value1State);
const resetValue2 = useResetRecoilState(value2State);
const updateStorage = useCallback(async () => {
if (!authToken) return;
const result = await saveToApiStorage(key, {value1, value2}, authToken);
if (result.failed) {
resetAuthToken();
resetValue1();
resetValue2();
}
}, [
authToken,
key,
resetAuthToken,
resetValue1,
resetValue2,
value1,
value2,
]);
// use void operator for correct return type
useEffect(() => void updateStorage(), [updateStorage]);
return null;
}
// include underneath the recoil root
function App () {
return (
<RecoilRoot>
<ApiStorageEffect />
{/* rest of app... */}
</RecoilRoot>
);
}

MSAL acquireTokenSilent followed by acquireTokenPopup results in a Bad Request in the popup

We are using MSAL.js to authenticate users to our Azure AD B2C instance. Users can be using a local account or sign in using their credentials from another Azure Active Directory instance.
Having signed in, our SPA gets an access token using acquireTokenSilent, with a fallback to acquireTokenPopup.
What we have noticed is that when acquireTokenSilent times out, the token may still be retrieved in the background, and the application local storage is updated with the token. However, in the app we have proceeded to call acquireTokenPopup. After the user enters their credentials in acquireTokenPopup, the popup displays "Bad Request". The user can close the popup and if they refresh the app, they will now be signed in.
This experience is not a great experience for our users.
Just wondering if this is this a known issue or is expected behavior?
Here is the relevant extract from our code. We wrap the UserAgentApplication in an MsalAuthenticationManager object.
function getMsalAuthenticationManager(authority: IMSALAuthorityConfig): IMsalAuthenticationManager {
return new MsalAuthenticationManager(
appConfig.msal.clientId,
authority.signUpOrSignInAuthority,
authority.passwordResetAuthority,
{
loadFrameTimeout: 15000,
endPoints: endPointsMap,
cacheLocation: appConfig.msal.cacheLocation // localStorage
}
);
}
// MsalAuthenticationManager constructor
constructor(
private applicationId: string,
authority?: string,
authorityForPasswordReset?: string,
msalOptions?: IMsalOptions
) {
var onTokenReceived = authorityForPasswordReset
? (errorDesc: string, token: string, error: string, tokenType: string) => {
// When the user clicks on the forgot password link, the following error is returned to this app.
// In this case we need to re-instantiate the UserAgentApplication object with the Password Reset policy.
if (errorDesc && errorDesc.indexOf("AADB2C90118") > -1) {
this._msal = new UserAgentApplication(
applicationId,
authorityForPasswordReset,
onTokenReceived,
msalOptions
);
this.signIn();
}
}
: (errorDesc: string, token: string, error: string, tokenType: string) => {};
this._msal = new UserAgentApplication(applicationId, authority, onTokenReceived, msalOptions);
this.acquireToken = this.acquireToken.bind(this);
this.signIn = this.signIn.bind(this);
this.signOut = this.signOut.bind(this);
this.getResourceForEndpoint = this.getResourceForEndpoint.bind(this); // Gets the scope for a particular endpoint
this.acquireToken = this.acquireToken.bind(this);
}
public acquireToken(scopes: string[]): Promise<string> {
return new Promise((resolve, reject) => {
this._msal
.acquireTokenSilent(scopes)
.then((accessToken: string) => resolve(accessToken))
.catch((acquireTokenSilentError: string) => {
this._msal
.acquireTokenPopup(scopes)
.then((accessToken: string) => resolve(accessToken))
.catch((acquireTokenPopupError: string) => reject(acquireTokenPopupError));
});
});
}
I had a similar issue, the reason being the token is still there but expired, and as msal.js doesn t check token expiration, you ll be seen as logged, but your token is actually invalid and your httpRequests with the bearer will fail as unauthorized. you should log the acquiretokenSilent error and look for "AADB2C90077" error, if the token is expired, call for a logout().

Auth0 connecting to Azure AD is not returning my state params

Using auth0, I'm following this tutorial about connecting a multi-tenant saas to Azure AD:
https://auth0.com/docs/tutorials/building-multi-tenant-saas-applications-with-azure-active-directory
But, since my case is a bit different, I need it to pass some parameters when the user logs in. For the other connections, I'm able to set options.authParams.state = "..." and this state is sent to my callbackURL.
But, using Azure AD connection, my state variable is not correct. It's empty when I receive the callback.
I'm including the new Azure AD button according to the tutorial:
lock.once('signup ready', function () {
var link = $('<div class="a0-zocial a0-icon a0-waad" href="#">'
+ '<span>Azure AD</span></div>');
link.on('click', function () {
lock.getClient().login({
connection: 'seedtec-onmicrosoft-com'
});
});
var iconList = $(this.$container).find('.a0-iconlist');
iconList.append(link);
});
And sending my state through options:
var options = {
container: 'root'
, callbackURL: 'http://.../LoginCallback.ashx'
, responseType: 'code'
, dict: 'pt'
, socialBigButtons: false
, authParams: {
state: state
, scope: 'openid profile'
, company_id: $scope.companyId
}
, mode: mode
, callbackOnLocationHash: true
};
lock.show(options);
Do you know what is wrong in that?
Thank you!
PS: I'm using angular libraries for auth0.
I actually found out the solution to this. It was quite simple, but it's not well documented by auth0.
When calling lock.getClient().login(...) I had to pass the authparams again like this:
lock.getClient().login({
connection: 'myconn-onmicrosoft-com'
, scope: 'openid profile'
, company_id: 'blah...'
, state: 'blah blah...'
});

Resources