office-addin-sso with #azure/msal-browser - azure-active-directory

Is it possible to use office-addin-sso with #azure/msal-browser ?
I would like to:
use OfficeRuntime.auth.getAccessToken() to get the Identity Token.
while at the same time use #azure/msal-browser as the fallback
method.
I have managed to get both the above working and can successfully get the MS Graph access token using just #azure/msal-browser.
Given that we want to use msal-browser/auth code flow with PKCE (and not msal/implicit flow) for the fallback, what would be the most effective way of getting the MS Graph access token in this context.
and given that the office-addin-sso package uses On Behalf Of Flow (requiring a secret and redirect).
Any help/suggestions or guidance would be really appreciated.

I use #azure/msal-browser in the office-addin-sso. My addin is for a single domain and the users are supposed to be logged in on OneDrive so I expect to never need the login via the fallback. I get the token silently from msal and then pass it to MS graph to get an access token. This is the code that does it in the ssoauthhelper.ts:
import * as Msal from '#azure/msal-browser';
export async function getToken_Email() {
try {
const msalConfig: Msal.Configuration = {
auth: {
clientId: "<your client id>", //This is your client ID
authority: "https://login.microsoftonline.com/<tenant id>", //The <tenant> in the URL is the tenant ID of the Azure Active Directory (Azure AD) tenant (a GUID), or its tenant domain.
redirectUri: "https://<your server>/office-js/fallbackauthdialog.html",
navigateToLoginRequestUrl: false,
},
cache: {
cacheLocation: "localStorage", // Needed to avoid "User login is required" error.
storeAuthStateInCookie: true, // Recommended to avoid certain IE/Edge issues.
},
};
const msalInstance = new Msal.PublicClientApplication(msalConfig);
const silentRequest = {
scopes: ["User.Read", "openid", "profile"]
};
let access_token: string;
try {
const loginResponse = await msalInstance.ssoSilent(silentRequest);
access_token = loginResponse.accessToken;
} catch (err) {
if (err instanceof Msal.InteractionRequiredAuthError) {
const loginResponse = await msalInstance.loginPopup(silentRequest).catch(error => {
// handle error
});
} else {
// handle error
}
}
console.log('silent token response: ' + JSON.stringify(access_token));
// makeGraphApiCall makes an AJAX call to the MS Graph endpoint. Errors are caught
// in the .fail callback of that call
const graph_response: any = await makeGraphApiCall(access_token);
console.log('graph response: ' + JSON.stringify(graph_response));
} catch (exception) {
console.log(exception);
}
}
export async function makeGraphApiCall(accessToken: string): Promise < any > {
try {
const response = await $.ajax({
type: "GET",
url: "https://graph.microsoft.com/oidc/userinfo/",
headers: {
access_token: accessToken,
Authorization: 'Bearer ' + accessToken + ' '
},
cache: false,
});
return response;
} catch (err) {
console.log(`Error from Microsoft Graph. \n${err}`);
}
}

Related

Need to differentiate login callback credentials from google and facebook using next-auth

I am getting data successfully from google and facebook login using next-auth. I want to save separate profiles for Google and Facebook as well. Data I am getting is name, image, email and session expiry but I need to differentiate the callback data. Maybe a provider name would work. I have tried to add provider in session and use it in page but in vain
Here is code for pages/api/auth/[...nextauth].js
import NextAuth from "next-auth"
import GoogleProvider from "next-auth/providers/google";
import FacebookProvider from "next-auth/providers/facebook";
export default NextAuth({
// Configure one or more authentication providers
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
authorization: {
params: {
prompt: "consent",
access_type: "offline",
response_type: "code"
}
}
}),
FacebookProvider({
clientId: process.env.FACEBOOK_CLIENT_ID,
clientSecret: process.env.FACEBOOK_CLIENT_SECRET
})
],
jwt: {
encryption: true
},
secret: process.env.SECRET,
callback: {
async jwt({ token, account }) {
// Persist the OAuth access_token to the token right after signin
if (account) {
token.accessToken = account.access_token;
}
if (account?.provider) {
token.provider = account.provider;
}
return Promise.resolve(token);
},
async session({ session, token }) {
// Send properties to the client, like an access_token from a provider.
session.accessToken = token.accessToken;
session.provider = token.provider;
return Promise.resolve(session);
},
// redirect: async(url, _baseUrl) => {
// if (url === "/profile") {
// return Promise.resolve("/");
// }
// return Promise.resolve("/");
// }
}
})
Here is code for login page
const {data:session} = useSession();
{JSON.stringify(session, 4)};
Result I get is
{"user":{"name":"myname","email":"myemail","image":"myimage"},"expires":"2022-09-04T16:40:10.809Z"};

Server-side authorization with JWT in SvelteKit

I have an issue sending a JWT token to the server and using it to authorize access in load handlers. I am using Firebase on the client for authentication. When logged in (onAuthStateChanged), I send a POST request with the token to the /api/login endpoint:
export async function post(req) {
const idToken = req.headers['authorization']
try {
const token = await firebase().auth().verifyIdToken(idToken)
req.locals.user = token.uid
} catch (e) {
console.log(e)
return {
status: 500,
body: 'forbidden',
}
}
return {
status: 200,
body: 'ok',
}
}
In hooks.js:
export function getSession(request) {
return {
user: request.locals.user
}
}
export async function handle({ request, resolve }) {
const cookies = cookie.parse(request.headers.cookie || '')
request.locals.user = cookies.user
const response = await resolve(request)
response.headers['set-cookie'] = `user=${request.locals.user || ''}; Path=/; HttpOnly`
return response
}
In load methods:
export async function load({ session }) {
if (!session.user) {
return {
status: 302,
redirect: '/start'
}
}
// ...
}
All of this works fine except that any client-side navigation after a login is rejected because session.user is still undefined. When navigating by typing the URL in the browser, it works correctly and after that the client-side navigation also works.
Any ideas why and what to do?
I have solved this by adding a browser reload on whichever page the user lands on after logging in. The snippet for the reload on the client side handling on a successful response from the login API endpoint looks like this
if (sessionLoginResponse?.status === "success") {
await signOut(auth);
window.history.back();
setTimeout(() => {
window.location.reload();
}, 10);
}

I'm using react-adal for Azure AD single sign in. It's token expires in 1hr. Is there any way to refresh the session or extending session expiry time

I've integrated Azure AD Single Sign in in my corporate react app using react-adal library for Azure AD single sign in. I've successfully implemented it but I'm facing one issue. It's token expires in 1hr because of which it logs out of the react web app. Is there any way to refresh the session or extending session expiry time.
import { AuthenticationContext } from 'react-adal';
const config = {
apiUrl: 'someUrl/',
graph_access_url: 'https://graph.microsoft.com',
graph_access_token_key: 'User_Graph_Token',
user_info_key: 'UserInfo'
};
const adalConfig = {
tenant: 'someTenant',
clientId: 'someclientId',
clientSecret: 'someclientSecret',
objectId: 'someObjectId',
endpoints: { api: 'someAPI' },
cacheLocation: 'localStorage',
redirectUri: window.location.origin,
azureRootUrl: 'https://login.microsoftonline.com',
issuerUrl: 'https://sts.windows.net'
};
const authContext = new AuthenticationContext(adalConfig);
function graphAccessToken() {
return localStorage[config.graph_access_token_key];
}
function azureRequest(url) {
let token = graphAccessToken();
const requestOptions = { method: 'GET', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + token } };
return fetch(url, requestOptions).then(response => response.json());
}
function getMe() {
return azureRequest('https://graph.microsoft.com/v1.0/me');
}
function login() {
authContext.login();
}
export function logout() {
localStorage.setItem(config.user_info_key, '');
localStorage.setItem(config.graph_access_token_key, '');
localStorage.clear();
authContext.logOut();
}
authContext.handleWindowCallback();
if (window === window.parent) {
if (!authContext.isCallback(window.location.hash)) {
if (authContext.getCachedToken(authContext.config.clientId) || authContext.getCachedUser()) {
authContext.acquireToken('https://graph.microsoft.com', (error, id_token) => {
if (id_token) {
localStorage.setItem(config.graph_access_token_key, id_token);
if (localStorage.getItem('adal.idtoken')) {
// Some Logic Implemented here.
}
}
});
}
}
}
The react-adal uses iframes for token silent refresh, you need to use the index.js as below.
The first token you generate has a 1 hour lifetime, when this token is near expiration, a refresh token will be retrieved by the library. By default, this library will try to refresh the token at least 5 minutes before the current token expiration date.
index.js:
import { runWithAdal } from 'react-adal';
import { authContext } from './adalConfig';
const DO_NOT_LOGIN = false;
runWithAdal(authContext, () => {
require('./indexApp.js');
},DO_NOT_LOGIN);
For more details, see the The frontend part of the blog.

'AADSTS500011' error message returned from API call using adalFetch

I have a React application that is registered in Azure Active Directory. In the API Permissions section, I have added permissions to access the API I am trying to access.
I am using the react-adal package to handle login and storage of access tokens when the user enters the app. My understanding is that the access token for the API is created at this point and adalFetch handles the logistics during the call to the API.
The response from the API is an error object (I replaced the actual id's; yes they match exactly and are correct in AAD):
{
message: "AADSTS500011: The resource principal named https://<domain>.onmicrosoft.com/APP_ID/access_as_user was not found in the tenant named TENANT. 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."
msg: "invalid_resource"
}
I have searched high and low to find a solution to why this isn't working. There is documentation on the API, but none specifying a resource or anything beyond the various endpoints i.e. http://thing-api.azurewebsites.net/api/endpointGoesHere
The API page states:
To use the API, apps need to implement modern authentication (OIDC) using AzureAD (AAD) and then request a token from AAD for the API.
The app id in Azure is https://domain.onmicrosoft.com/APP_ID and requires the “access_as_user” scope.
adalConfig.js
import { AuthenticationContext, adalFetch, withAdalLogin } from 'react-adal';
export const adalConfig = {
clientId: CLIENT_ID,
tenant: TENANT,
endpoints: {
thingApi: 'https://<domain>.onmicrosoft.com/APP_ID/access_as_user',
graphApi: 'https://graph.microsoft.com',
},
cacheLocation: 'localStorage',
};
export const authContext = new AuthenticationContext(adalConfig);
export const adalApiFetch = (fetch, url, options) =>
adalFetch(authContext, adalConfig.endpoints.thingApi, fetch, url, options);
export const adalGraphFetch = (fetch, url, options) =>
adalFetch(authContext, adalConfig.endpoints.graphApi, fetch, url, options);
Function for the API call. Executed in componentDidMount.
TrainLanding.jsx
//Returns error
fetchData = () => {
adalApiFetch(fetch, 'http://thing-api.azurewebsites.net/api/EventGet', {})
.then((response) => {
response.json()
.then((responseJson) => {
this.setState({ apiResponse: JSON.stringify(responseJson, null, 2) }, () => {
console.log(this.state.apiResponse)
})
});
})
.catch((error) => {
console.error(error);
})
}
//works perfectly fine
fetchGraph = () => {
adalGraphFetch(fetch, 'https://graph.microsoft.com/v1.0/me', {})
.then((response) => {
response.json()
.then((responseJson) => {
this.setState({ apiResponse: JSON.stringify(responseJson, null, 2) }, () => {
console.log(this.state.apiResponse)
})
});
})
.catch((error) => {
console.error(error);
})
}
I set up a graph API call in the exact same way to test the method, and it works perfectly fine. So I know adal is set up correctly, I just don't understand the error and where I am going wrong. My googling has not yielded any useful results.
Ok, so if you're here, some things to note:
Don't use ADAL. Use MSAL. ADAL is v1 and does not work. Read here for examples: https://www.npmjs.com/package/react-aad-msal
You should wrap your entire app inside the component you get from above. I will show how I did it below.
You must have already registered your app in Azure Active Directory, configured redirect URLs, and included API permissions.
index.js
import { AzureAD, MsalAuthProviderFactory, LoginType } from 'react-aad-msal';
import { msalConfig, authParams } from './msalConfig';
class Index extends Component {
state = {
userInfo: null,
}
userJustLoggedIn = (accInfo) => {
this.setState({
userInfo: accInfo
})
}
render() {
return(
<AzureAD
provider={
new MsalAuthProviderFactory(msalConfig, authParams, LoginType.Redirect)
}
forceLogin={true}
accountInfoCallback={this.userJustLoggedIn}
>
<HashRouter>
<App userInfo={this.state.userInfo}/>
</HashRouter>
</AzureAD>
);
}
}
ReactDOM.render(
<Index/>, document.getElementById('root')
);
This might not be what your index looks like if you are using the most recent version of Create React App. I converted the Index into a component for a couple of reasons. First, the authentication loop for me was getting stuck 1 refresh short when redirecting. Second, so I could store the logged in user's info in state, update with setState (which forces another render), and then pass it as a prop to the rest of my app.
msalConfig.js
export const msalConfig = {
auth: {
authority: process.env.REACT_APP_AUTHORITY, //this should be "https://login.microsoftonline.com/<your-tenant-id>"
clientId: process.env.REACT_APP_CLIENT_ID, //just "<your-client-id>"
redirectUri: process.env.REACT_APP_REDIRECT //"<url of your app or localhost port you dev on>"
},
cache: {
cacheLocation: "localStorage",
storeAuthStateInCookie: true
}
};
export const authParams = {
//can be whatever api scopes you need here **as long as they are from the same API address**
scopes: [
'https://graph.microsoft.com/User.ReadBasic.All',
'https://graph.microsoft.com/email',
'https://graph.microsoft.com/profile',
'https://graph.microsoft.com/User.Read'
],
extraScopesToConsent: [
//any non Microsoft Graph API scopes go here for this example
'any extra strings of APIs to consent to'
]
}
Read above env files and variables here: https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#what-other-env-files-can-be-used
I have a .env.development and a .env.production with the proper redirect URLs for each.
After you have authenticated the user, you can access the API.
You need to acquire a token silently before each API call and use the token in the request. For me it looks like this:
const authProvider = new MsalAuthProviderFactory(msalConfig, authParams);
console.log(authProvider)
authProvider.getAuthProvider().UserAgentApplication.acquireTokenSilent(authParams)
.then((res) => {
axios({
headers: {
'Authorization': 'Bearer ' + res.accessToken
},
method: 'GET',
url: "api address"
})
.then((response) => {
//do stuff with response
console.log(response)
})
.catch((error) => {
console.log('axios fail: ' + error)
})
})
.catch((error) => {
console.log('token fail: ' + error)
})
I put this into a function and called during componentDidMount.
I will update if anything changes. I hope this helps someone.

Authentication Refresh Token

Building a login form using Reactjs, redux, axios and redux-thunk. I have two tokens - one named access token and refresh token.
When the user is authenticated, store the access token which should last for 12 hours. The refresh token is also provided and will last 30 days.
Once the access token has expired need to check the timestamp (date) if access token is expired.
How can I update the access token once expired? Token data looks like this so I have a timestamp to check against:
{
"access_token": "toolongtoinclude",
"token_type": "bearer",
"refresh_token": "toolongtoinclude",
"expires_in": 43199,
"scope": "read write",
"roles": [
"USER"
],
"profile_id": "b4d1e37d-7d05-4eb3-98de-0580d3074a0d",
"jti": "e975db65-e3b7-4034-a6e4-9a3023c3d175"
}
Here are my actions to save, get and update tokens from storage. I'm just unsure on how to refresh the token.
export function submitLoginUser(values, dispatch) {
dispatch({type: constants.LOADING_PAGE, payload: { common: true }})
return axios.post(Config.API_URL + '/oauth/token', {
username: values.email,
password: values.password,
scope: Config.WEBSERVICES_SCOPE,
grant_type: Config.WEBSERVICES_GRANT_TYPE_PASSWORD
},
{
transformRequest: function (data) {
var str = [];
for (var p in data) {
str.push(encodeURIComponent(p) + '=' + encodeURIComponent(data[p]));
}
return str.join('&');
},
headers: {
'Authorization': 'Basic ' + window.btoa(Config.WEBSERVICES_AUTH),
'Content-Type': 'application/x-www-form-urlencoded'
}
})
.then(response => {
const {access_token, refresh_token} = response.data;
dispatch({type: constants.LOADING_PAGE, payload: { common: false }})
dispatch({
type: constants.LOGIN_SUCCESS,
payload: {
access_token: access_token,
refresh_token: refresh_token
}
});
saveTokens(response)
browserHistory.push('/questions');
refreshToken(response);
})
.catch(error => {
dispatch({type: constants.LOADING_PAGE, payload: { common: false }})
//401 Error catch
if(error.response.status === 401) {
throw new SubmissionError({username: 'User is not authenticated', _error: message.LOGIN.loginUnAuth})
}
//Submission Error
throw new SubmissionError({username: 'User does not exist', _error: message.LOGIN.loginFailed})
})
}
/**
* Save tokens in local storage and automatically add token within request
* #param params
*/
export function saveTokens(params) {
const {access_token, refresh_token} = params.data;
localStorage.setItem('access_token', access_token);
if (refresh_token) {
localStorage.setItem('refresh_token', refresh_token);
}
//todo fix this later
getinstanceAxios().defaults.headers.common['Authorization'] = `bearer ${access_token}`
}
/**
*
*/
export function getTokens() {
let accessToken = localStorage.getItem('access_token');
return accessToken
}
/**
* update the get requests
*/
export function updateTokenFromStorage() {
const tokenLocalStorage = getTokens();
getinstanceAxios().defaults.headers.common['Authorization'] = `bearer ${tokenLocalStorage}`;
}
/**
* Refresh user access token
*/
export function refreshToken(dispatch) {
//check timestamp
//check access expired - 401
//request new token, pass refresh token
//store both new access and refresh tokens
}
check this out:
https://github.com/mzabriskie/axios#interceptors
I think this can help you. You intercept your request and make your validations.
EDIT
Here is the code I've tried to use in my store to test, not getting any log back
import { createStore, applyMiddleware, compose } from 'redux'
import { devTools, persistState } from 'redux-devtools'
import axios from 'axios'
import Middleware from '../middleware'
import Reducer from '../reducers/reducer'
import DevTools from '../containers/DevTools'
let finalCreateStore
if (__DEVELOPMENT__ && __DEVTOOLS__) {
finalCreateStore = compose(
applyMiddleware.apply(this, Middleware),
// Provides support for DevTools:
DevTools.instrument(),
// Optional. Lets you write ?debug_session=<key> in address bar to persist debug sessions
persistState(getDebugSessionKey())
)(createStore)
} else {
finalCreateStore = compose(
applyMiddleware.apply(this, Middleware)
)(createStore)
}
function getDebugSessionKey() {
// You can write custom logic here!
// By default we try to read the key from ?debug_session=<key> in the address bar
const matches = window.location.href.match(/[?&]debug_session=([^&]+)\b/)
return (matches && matches.length > 0)? matches[1] : null
}
axios.interceptors.response.use((err) => {
if (err.status === 401) {
console.log('ACCESS TOKEN EXPIRED!');
}
});
export const store = finalCreateStore(Reducer)

Resources