Office UI Outlook addin using auth is unstable - reactjs

We're currently developing a Office UI addin using React. The addin should make a connection with a backend api and authenticate the user using bearer tokens. The backend api is protected by Azure AD.
We based our solution on the example that is offered by Microsoft: https://github.com/OfficeDev/PnP-OfficeAddins/tree/master/Samples/auth/Office-Add-in-Microsoft-Graph-React This uses msal.js for the authentication.
The login dialog is opened like so:
await Office.context.ui.displayDialogAsync(dialogLoginUrl, { height: 40, width: 30 }, result => {
if (result.status === Office.AsyncResultStatus.Failed) {
displayError(`${result.error.code} ${result.error.message}`);
} else {
loginDialog = result.value;
loginDialog.addEventHandler(Office.EventType.DialogMessageReceived, processLoginMessage);
loginDialog.addEventHandler(Office.EventType.DialogEventReceived, processLoginDialogEvent);
}
});
And the following code runs within the dialog:
import { UserAgentApplication } from "msal";
(() => {
// The initialize function must be run each time a new page is loaded
Office.initialize = () => {
const config = {
auth: {
clientId: "",
authority: "",
redirectUri: "https://localhost:3000/login.html",
navigateToLoginRequestUrl: false
},
cache: {
cacheLocation: "localStorage",
storeAuthStateInCookie: false
}
};
const userAgentApp = new UserAgentApplication(config);
const authCallback = (error, response) => {
if (!error) {
if (response.tokenType === "id_token") {
localStorage.setItem("loggedIn", "yes");
} else {
// The tokenType is access_token, so send success message and token.
Office.context.ui.messageParent(JSON.stringify({ status: "success", result: response.accessToken }));
}
} else {
const errorData = `errorCode: ${error.errorCode}
message: ${error.errorMessage}
errorStack: ${error.stack}`;
Office.context.ui.messageParent(JSON.stringify({ status: "failure", result: errorData }));
}
};
userAgentApp.handleRedirectCallback(authCallback);
const request = {
scopes: ["api://..."]
};
if (localStorage.getItem("loggedIn") === "yes") {
userAgentApp.acquireTokenRedirect(request);
} else {
// This will login the user and then the (response.tokenType === "id_token")
// path in authCallback below will run, which sets localStorage.loggedIn to "yes"
// and then the dialog is redirected back to this script, so the
// acquireTokenRedirect above runs.
userAgentApp.loginRedirect(request);
}
};
})();
Unfortunately this doesn't seem lead to a stable addin. The authentication dialog sometimes works as expected, but sometimes it doesn't. In Outlook on macOS it seems to work fine, but in Outlook on Windows the handling of the callback is not always working correctly. Also in the web version of Outlook it doesn't work as expected.
The question is whether someone has a working solution using React and msal.js in a Outlook addin.

Related

Cookies not showing up

I am having trouble getting my express server to attach a cookie on login. Bellow is the code I have written, I recently deployed my react app on Vercel which is what is making requests to my express server which I am still running locally. Everything works as it should and cookies are attaches when I run the react app locally.
Thank you,
Express Server
router.post("/", (req, res) =>{
const email = req.body.userName.toLowerCase()
const pass = req.body.password
db.query(`SELECT * FROM users WHERE email = '${email}'`, async (err, result) => {
if(result.rows.length === 1){
let user = result.rows[0]
bcrypt.compare(pass, user.password, (err, result) => {
if (err) throw err;
if(result === true){
user.password = 'blocked'
let token = jwt.sign({"tokenInfo": user, "userType": user.user_type}, secret, { algorithm: 'HS256'})
return res.cookie('userId', token, {
maxAge: 60000 * 60 * 2,
httpOnly: false
}).send({'message': "User Loged In", 'userType': user.user_type, 'userID': user.apaid})
} else {
return res.send({'message': 'Invalid Password', 'userType': 'false'})
}
})
} else if(result.rows.length > 1) {
res.send('WTF')
} else {
res.send({'message': 'Invalid User Name', 'userType': 'false'})
}
console.log(res.cookies)
})
})
Frontend and backend parts of your app should be served on the same domain in order to use cookies. If you created your app with create-react-app you can set up a proxy in your package.json file of your react app.
"devDependencies": {
...
},
"proxy": "http://*address of your locally running server eg:localhost:5000/*"

How to use navigator.credentials to store passwords in a React application?

In a react app I need to access MySQL servers, for which I need the user's credentials. In order to avoid having the user enter them every time a connection is opened I'd like to store the password in the browser's password store/manager.
According to MDN all major browsers should support the Credentials Management API. So I tried it like this:
private requestPassword = (commandId: string, args?: any[]): void => {
if (args && args.length > 0) {
// First check if we already have a password and don't ask for it, if so.
const request: IServicePasswordRequest = args[0];
navigator.credentials.get({
password: true,
mediation: "silent",
} as any).then((credential) => {
if (credential) {
const id = 0;
} else {
// Opens the password dialog.
this.setState({ request }, () => this.dialogRef.current?.open());
}
});
}
};
private closePanel = (e: React.SyntheticEvent, props: IButtonProperties): void => {
// Called when either OK or Cancel was clicked by the user, in the password dialog.
if (props.id === "ok") {
const { request } = this.state;
const options = {
password: {
id: request!.serviceId,
name: request!.user,
password: "root", // Just for test.
},
};
navigator.credentials.create(options as any).then((credential) => {
if (credential) {
navigator.credentials.store(credential).then((value) => {
console.log("Success: " + value);
}).catch((e) => {
console.log("Error: " + e);
});
}
});
}
this.dialogRef.current?.close();
};
However there are several problems with that:
The password member (as documented on the CredentialContainer.create() page is unknown to Typescript. I worked around that with an any cast. The returned credential is a PasswordCredential structure, and the content looks fine.
When storing the credentials, the success branch is taken but the promise value is null. Not sure if that's relevant at all.
When I call navigator.credentials.get I never get any credential back. And in fact I'm not surprised. Shouldn't it be necessary to pass in id and user name to find credentials? But the API doesn't allow that.
So, what's the correct approach here?

Firebase functions not being invoked

I am trying to integrate Stripe payments on my webapp using Firebase. I have cloned the code from the repository here: https://github.com/firebase/functions-samples/tree/master/stripe and have followed the documentation here: https://firebase.google.com/docs/use-cases/payments
From reading the documentation, I assumed that when a customer signed in through firebase authentication, their details would be added to a stripe_customer collection in the firestore. I realised this wasn't the case, and manually added a user to test the save card functions. Then I received the following error : "Invalid value for stripe.confirmCardSetup intent secret: value should be a client_secret string. You specified: undefined"
I have a blaze plan for firebase and have configured. From following the steps in the documentation, I assumed this would be working. I'm sorry this question is so vague, but it seems at every corner I'm getting another issue. Is there something very obvious I am missing that is stopping this code from working? I am trying to implement this for a friends business as a favor, and am getting really confused with Firebase. I am coding in Angularjs. Would greatly appreciate any help on this!
This is the code for the function to create a customer
exports.createStripeCustomer = functions.auth.user().onCreate(async (user) => {
const customer = await stripe.customers.create({ email: user.email });
const intent = await stripe.setupIntents.create({
customer: customer.id,
});
await admin.firestore().collection('stripe_customers').doc(user.uid).set({
customer_id: customer.id,
setup_secret: intent.client_secret,
});
return;
});
And this is the code being called in the controller:
const firebaseUI = new firebaseui.auth.AuthUI(firebase.auth());
const firebaseUiConfig = {
callbacks: {
signInSuccessWithAuthResult: function (authResult, redirectUrl) {
// User successfully signed in.
// Return type determines whether we continue the redirect automatically
// or whether we leave that to developer to handle.
return true;
},
uiShown: () => {
document.getElementById('loader').style.display = 'none';
},
},
signInFlow: 'popup',
signInSuccessUrl: '/checkout.html',
signInOptions: [
firebase.auth.GoogleAuthProvider.PROVIDER_ID,
firebase.auth.EmailAuthProvider.PROVIDER_ID,
],
credentialHelper: firebaseui.auth.CredentialHelper.NONE,
// Your terms of service url.
tosUrl: 'https://example.com/terms',
// Your privacy policy url.
privacyPolicyUrl: 'https://example.com/privacy',
};
firebase.auth().onAuthStateChanged((firebaseUser) => {
if (firebaseUser) {
currentUser = firebaseUser;
firebase
.firestore()
.collection('stripe_customers')
.doc(currentUser.uid)
.onSnapshot((snapshot) => {
if (snapshot.data()) {
customerData = snapshot.data();
startDataListeners();
document.getElementById('loader').style.display = 'none';
document.getElementById('content').style.display = 'block';
} else {
console.warn(
`No Stripe customer found in Firestore for user: ${currentUser.uid}`
);
}
});
} else {
document.getElementById('content').style.display = 'none';
firebaseUI.start('#firebaseui-auth-container', firebaseUiConfig);
}
});
The error you've supplied (below) implies that the key in your config isn't been pulled into your code. If you're running this locally you need to run the below any time you change your functions:config values.
firebase functions:config:get > .runtimeconfig.json
Check the doc's out about how to run your function locally:
Error
"Invalid value for stripe.confirmCardSetup intent secret: value should
be a client_secret string. You specified: undefined"

Redirect to same URL at client side after authentication from token server (identity server 4)

I am using Identity server 4 for user identity and token service. And my client application written in .Net core React template. Everything is working fine, however when end user hit the client side sub URL page (received from email), it is redirecting to STS identity server for authentication and return back to home page instead of to Sub page from where user hit the URL in the begging.
example when user hit client side URL (https://localhost:44309/bills)(which is received through email) it is going login page at (https://localhost:44318/Login) and after user authentication it is redirecting to (https://localhost:44309/Home) instead of (https://localhost:44309/bills).
I used Identity server 4 code written similar to below link
https://github.com/damienbod/IdentityServer4AspNetCoreIdentityTemplate/tree/master/content/StsServerIdentity
Identity server added client
{
"ClientId": "reactclient",
"ClientName": "React Client",
"Enabled": true,
"RequireClientSecret": false,
"EnableLocalLogin": true,
"RequireConsent": false,
"AllowedGrantTypes": [ "authorization_code", "hybrid", "client_credentials" ],
"RedirectUris": [ "https://localhost:44309/signin-oidc" ],
"PostLogoutRedirectUris": [ "https://localhost:44309/logout/callback" ],
"AccessTokenType": "Jwt",
"AllowAccessTokensViaBrowser": true,
//"UpdateAccessTokenClaimsOnRefresh": true,
"AllowOfflineAccess": true,
"AccessTokenLifetime": 14400,
"IdentityTokenLifetime": 7200,
"AllowedScopes": [
"openid",
"profile",
"email",
"offline_access"
]
}
Client side
export const IDENTITY_CONFIG = {
authority: process.env.REACT_APP_AUTH_URI,
client_id: process.env.REACT_APP_IDENTITY_CLIENT_ID,
redirect_uri: process.env.REACT_APP_BASE_URI + process.env.REACT_APP_REDIRECT_PATH,
automaticSilentRenew: true,
filterProtocolClaims: true,
loadUserInfo: true,
silent_redirect_uri: process.env.REACT_APP_BASE_URI + process.env.REACT_APP_SILENT_REDIRECT_PATH,
post_logout_redirect_uri: process.env.REACT_APP_BASE_URI + process.env.REACT_APP_LOGOFF_REDIRECT_PATH,
response_type: 'code',
scope: process.env.REACT_APP_SCOPE
};
"base": {
"REACT_APP_TESTENV": "1",
"REACT_APP_IDENTITY_CLIENT_ID": "reactclient",
"REACT_APP_REDIRECT_PATH": "signin-oidc",
"REACT_APP_SILENT_REDIRECT_PATH": "silentrenew",
"REACT_APP_LOGOFF_REDIRECT_PATH": "logout/callback",
"REACT_APP_SCOPE": "openid profile email",
"NODE_TLS_REJECT_UNAUTHORIZED": "0"
},
"development": {
"REACT_APP_TESTENV": "development",
"REACT_APP_AUTH_URI": "https://localhost:44318",
"REACT_APP_AUTH_ISSUER": "https://localhost:44318",
"REACT_APP_BASE_URI": "https://localhost:44309/",
"REACT_APP_SERVICE_MEMBER_BASE_URI": "https://localhost:44320/"
},
Identity server side code. similar to https://github.com/IdentityServer/IdentityServer4.Demo/blob/master/src/IdentityServer4Demo/Quickstart/Account/AccountController.cs
public async Task<IActionResult> Login(LoginInputModel model)
{
var returnUrl = model.ReturnUrl;
ViewData["ReturnUrl"] = returnUrl;
if (ModelState.IsValid)
{
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
//
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberLogin, lockoutOnFailure: false);
if (result.Succeeded)
{
Logit("User logged in.");
return RedirectToLocal(returnUrl);
}
else if (result.RequiresTwoFactor)
{
return RedirectToAction(nameof(VerifyCode), new { ReturnUrl = returnUrl, RememberMe = model.RememberLogin });
}
else if (result.IsLockedOut)
{
Logit("User account locked out.");
return View("Lockout");
}
else
{
//check if user exists in BIZZ db
ModelState.AddModelError(string.Empty, _sharedLocalizer["INVALID_LOGIN_ATTEMPT"]);
return View(await BuildLoginViewModelAsync(model));
}
}
// If we got this far, something failed, redisplay form
return View(await BuildLoginViewModelAsync(model));
}
Can anyone explain how I can redirect to specific page instead of going to home page each time after login. I would want to solve this at identity server instead at client side.
You have to use history in reactjs to get previous path and need to save in sessionStorage.
May be this will help you :
const SAVED_URI = 'APP_PLU';
const FORBIDDEN_URIS = ['/login-response', '/'];
const DEFAULT_URI = '/';
function setPostLoginUri() {
// using just the pathname for demo, should be more detailed in production to
// include query params, hash bangs, etc
const currentPath = window.location.pathname;
const savedURI = sessionStorage.getItem(SAVED_URI);
if (FORBIDDEN_URIS.includes(currentPath) || savedURI) {
return;
}
sessionStorage.setItem(SAVED_URI, currentPath);
}
function getPostLoginUri(retain) {
const savedURI = sessionStorage.getItem(SAVED_URI);
if (!retain) {
sessionStorage.removeItem(SAVED_URI);
}
return savedURI || DEFAULT_URI;
}
export default {
get: getPostLoginUri,
set: setPostLoginUri
};
And set in the app.js
and then in your login response page add this code ,
function LoginResponse({ history, setUser }) {
const [error, setError] = useState(null);
useEffect(() => {
// the login redirect has been completed and we call the
// signinRedirectCallback to fetch the user data
userManager.signinRedirectCallback().then(user => {
// received the user data so we set it in the app state and push the
// router to the secure or bookmarked route
setUser(user);
history.push(postLoginUri.get());
}, ({ message }) => {
// userManager throws if someone tries to access the route directly or if
// they refresh on a failed request so we just send them to the app root
if (message && redirectErrors.includes(message)) {
history.push('/');
return;
}
// for all other errors just display the message in production it would be
// a good idea to initiate a sign out after a countdown
setError(message);
});
}, []);
return error;
}
export default LoginResponse;

Amplify - GraphQL request headers are empty

I am attempting to create an app that utilizes Cognito user pools for user auth and then sending api requests to a dynamoDB table through graphQL.
The user auth/signup works correctly, however I receive a 401 error when attempting to query a data table. The message states "Missing authorization header"
I saw in a similar post that the auth token should be auto-populated into the request headers, but that does not occur for me. I also saw that Amplify created a function for custom graphql headers. I attempted this also but still get the same "Missing authorization header" error.
Any suggestions?
aws_appsync_graphqlEndpoint:'',
aws_appsync_region:'',
aws_appsync_authenticationType:'AMAZON_COGNITO_USER_POOLS',
graphql_headers: async () => ({
'My-Custom-Header': cognitoUser
})
}
This is in my config/exports file for amplify ---- Amplify.configure(config)
if (cognitoUser != null) {
cognitoUser.getSession((err, session) => {
if (err) {
console.log(err);
} else if (!session.isValid()) {
console.log("Invalid session.");
} else {
console.log( session.getIdToken().getJwtToken());
}
});
} else {
console.log("User not found.");
}
console.log(cognitoUser)
Amplify.configure(config)
const client = new AWSAppSyncClient({
disableOffline: true,
url: config.aws_appsync_graphqlEndpoint,
region: config.aws_appsync_region,
identityPoolId: config.aws_cognito_identity_pool_id,
userPoolId: config.aws_user_pools_id,
userPoolWebClientId: config.ws_user_pools_web_client_id,
auth: {
type: config.aws_appsync_authenticationType,
jwtoken: async () =>
(await Auth.currentSession()).getIdToken().getJwtToken(),
apiKey: config.aws_appsync_apiKey
}
});```
This is my client settings in my index.js folder
I apologize if I missed something blatant. I am new to backend and am having trouble with getting this to work.
I have only gotten it to work when using API_Key auth.

Resources