Getting a Azure AD refresh token and auth code using MSAL - azure

I am working on an application with Ruby on Rails back-end and EmberJS Front-end.
I would like to achieve the following.
Log in user with MSAL with Ember FE
Get the auth_code and pass on to the back end
From the back end, fetch access token and refresh token.
Use it to send email using azure Graph API.
Basically I would like to perform step 1 of azure ad auth, i.e. fetching the auth_code, at the front end. And then perform step 2 at the back end. And then refresh the tokens at the back-end as needed.
I believe MSAL provides no good way to achieve this. When user consents (Step 1), it uses the auth_code by itself to fetch access_token and refresh_token (Step 2), and cashes it without exposing the values. It invokes acquireTokenSilent method to refresh the tokens when needed.
I can see that MSAL also has a ssoSilent method, which performs only step 1 of auth and returns auth code. I tried to use it in the following manner
signIn = () => {
myMSALObj
.loginPopup(loginRequest)
.then(this.handleResponse)
.catch((error) => {
console.error(error);
});
};
handleResponse = (resp) => {
console.log('>>>> Response', resp);
// this.token = resp;
if (resp !== null) {
username = resp.account.username;
// showWelcomeMessage(resp.account);
resp.account = myMSALObj.getAccountByUsername(username);
console.log('Resp => ', resp);
myMSALObj
.ssoSilent(resp)
.then((r) => {
console.log(r);
})
.catch((e) => {
console.log('Error ->', e);
});
} else {
// loadPage();
}
};
This always ends up in the following error
InteractionRequiredAuthError: interaction_required: Silent authentication was denied. The user must first sign in and if needed grant the client application access to the scope ...
Even when the user has just consented for these scopes.
Is there something I am missing?
or
Is there any better way to achieve this functionality?
Thanks in advance for any help
I am using Rails 6 with Ember 4.1

Related

Are the scopes defined when 'exposing an API' in an Azure AD Application Registration for information purposes only? If not, how are they enforced?

Desired Behaviour
I am setting up a Node.js web application using Azure AD B2C authentication and authorisation.
It is a confidential, server side, client (i.e. - not a Single Page Application).
The desired behaviour is:
Authenticate and authorise a user via login using Azure ADB2C
Based on a successful login, allow users to call routes in the Node web app
My specific question is at the bottom of this post, but is essentially:
Given that, if an accessToken is present in req.session, my
application will return the result from calling ANY endpoint I choose
(even if it is not related to any of the 'scopes' defined when
'exposing the api'), are the scopes that are defined when 'exposing an
API' essentially 'for information purposes only' - both for
application admins and end users? Or should they somehow be enforced in each relevant Express route handler?
Research
I have done extensive reading and video watching around the topic, including:
GitHub Repositories:
active-directory-b2c-msal-node-sign-in-sign-out-webapp
active-directory-b2c-javascript-nodejs-webapi
learn.microsoft.com articles:
https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-overview
https://learn.microsoft.com/en-au/azure/active-directory-b2c/configure-a-sample-node-web-app
https://docs.microsoft.com/azure/active-directory-b2c/enable-authentication-in-node-web-app
https://docs.microsoft.com/azure/active-directory-b2c/configure-authentication-in-sample-node-web-app-with-api
https://docs.microsoft.com/azure/active-directory-b2c/enable-authentication-in-node-web-app-with-api
Videos:
Identity for Developers Playlist by Microsoft Security
OAuth 2.0 and OpenID Connect (in plain English) by OktaDev
What I've Tried
I have been able to implement a basic prototype consisting of:
Node.js web application (a confidential, server side, client, not a Single Page App)
Sign Up, Sign In and Edit Profile User flows
2 x Application Registrations in Azure - one for the Web App and one for the Web App API
I have 'exposed the API' in the Web App API registration, eg: :
https://my-tenant-name.onmicrosoft.com/my-api-uri-thing/tasks.read
https://my-tenant-name.onmicrosoft.com/my-api-uri-thing/tasks.write
I have 'added permissions' to the Web App registration (to access the Web App API registration), eg:
MY-APP-API (2)
tasks.read, Delegated, Admin Consent Required - Yes, Granted for MY-APP
tasks.write, Delegated, Admin Consent Required - Yes, Granted for MY-APP
Microsoft Graph (2)
offline_access, Delegated, Admin Consent Required - No, Granted for MY-APP
openid, Delegated, Admin Consent Required - No, Granted for MY-APP
So the workflow is as follows (based on this code):
User clicks on a '/signin' link
The relevant Express route handler passes through the required scopes to getAuthCode():
app.get('/signin',(req, res)=>{
//Initiate a Auth Code Flow >> for sign in
//Pass the api scopes as well so that you received both the IdToken and accessToken
getAuthCode(process.env.SIGN_UP_SIGN_IN_POLICY_AUTHORITY,apiConfig.webApiScopes, APP_STATES.LOGIN, res);
});
Source
The value of the scopes parameter is:
['https://${process.env.TENANT_NAME}.onmicrosoft.com/my-api-uri-thing/tasks.read','https://${process.env.TENANT_NAME}.onmicrosoft.com/my-api-uri-thing/tasks.write']
Upon successful sign in, the user is redirected to /redirect
The relevant Express route handler passes through a tokenRequest object to get an accessToken which is then added to req.session:
app.get('/redirect',(req, res)=>{
if (req.query.state === APP_STATES.LOGIN) {
// prepare the request for calling the web API
tokenRequest.authority = process.env.SIGN_UP_SIGN_IN_POLICY_AUTHORITY;
tokenRequest.scopes = apiConfig.webApiScopes;
tokenRequest.code = req.query.code;
confidentialClientApplication.acquireTokenByCode(tokenRequest)
.then((response) => {
req.session.accessToken = response.accessToken;
req.session.givenName = response.idTokenClaims.given_name;
console.log('\nAccessToken:' + req.session.accessToken);
res.render('signin', {showSignInButton: false, givenName: response.idTokenClaims.given_name});
}).catch((error) => {
console.log(error);
res.status(500).send(error);
});
}else{
res.status(500).send('We do not recognize this response!');
}
});
User calls a 'protected API'
The relevant Express route handler checks if req.session contains an accessToken value
If the accessToken is present, it makes an http request to the desired endpoint using axios and passes through the accessToken as the Bearer token in the headers of the request.
The 'protected content' is then returned
app.get('/api', async (req, res) => {
if(!req.session.accessToken){
//User is not logged in and so they can only call the anonymous API
try {
const response = await axios.get(apiConfig.anonymousUri);
console.log('API response' + response.data);
res.render('api',{data: JSON.stringify(response.data), showSignInButton: true, bg_color:'warning'});
} catch (error) {
console.error(error);
res.status(500).send(error);
}
}else{
//Users have the accessToken because they signed in and the accessToken is still in the session
console.log('\nAccessToken:' + req.session.accessToken);
let accessToken = req.session.accessToken;
const options = {
headers: {
//accessToken used as bearer token to call a protected API
Authorization: `Bearer ${accessToken}`
}
};
try {
const response = await axios.get(apiConfig.protectedUri, options);
console.log('API response' + response.data);
res.render('api',{data: JSON.stringify(response.data), showSignInButton: false, bg_color:'success', givenName: req.session.givenName});
} catch (error) {
console.error(error);
res.status(500).send(error);
}
}
});
Question
Given that, if an accessToken is present in req.session, my application will return the result from calling ANY endpoint I choose (even if it is not related to any of the 'scopes' defined when 'exposing the api'), are the scopes that are defined when 'exposing an API' essentially 'for information purposes only' - both for application admins and end users?
Or should I be coding in a conditional statement in each relevant Express route handler that says:
IF the required scope for accessing this content is present in this access token,
THEN you can access this content
and therefore 'enforces' the scope that has been defined and consented to by the user.
With my current level of understanding, I am assuming that these scopes ARE just for information purposes only, because I haven't seen any examples where the scopes are enforced through code.

How to access jwt after login using msal.js adb2c

After logging in using the MsalAuthenticationTemplate InteractionType.Redirect, how do I get the JWT returned after successful authentication? It does not seem to be included in the msal instance.
import { MsalProvider, MsalAuthenticationTemplate, useMsal, useAccount } from "#azure/msal-react";
const { instance } = useMsal();
You should call acquireTokenSilent each time you need an access token. You can read more in our getting started doc and also review the msal-react-samples
Another way of getting the idToken(JWT) after successful login is to hook into the addEventCallback and check for EventType.LOGIN_SUCCESS.
const callbackId = instance.addEventCallback(message => {
if (message.eventType === EventType.LOGIN_SUCCESS) {
const { payload } = message;
// Get idToken(JWT) from the payload.
console.log(payload.idToken);
}
})
https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/events.md

Firebase custom claims doesn't seem to work

I have an issue with my project - Node.js and Firebase.
I want to add an admin role so I use custom claims according to the docs.
I have 3 functions in the cloud functions:
exports.addAdminRole = functions.https.onCall((data, context) => {
return admin.auth().setCustomUserClaims(context.auth.uid, {
admin: true
}).then(() =>'Success!')
.catch(err => err)
})
exports.getUserRecord = functions.https.onCall((data, context) => {
return admin.auth().getUser(context.auth.uid)
.then(userRecord => userRecord.customClaims)
.catch(err => err)
})
exports.deleteAdmin = functions.https.onCall((data, context) => {
return admin.auth().setCustomUserClaims(context.auth.uid, null)
.then(() => 'Deleted admin!')
.catch(err => err)
})
I call the functions directly in the client (http callable) and addAdminRole returns 'Success!' and seems to work. getUserRecord seems to work as well and returns {admin: true} as my custom claims.
Now for the problem. I defined a function to get the user claims in the client side like mentioned in the docs:
getRecords() {
firebaseInstance.auth.currentUser.getIdTokenResult()
.then(idTokenResult => {console.log("ADMIN:", idTokenResult)})
.catch(err => console.error(err))
}
(firebaseInstance is imported and works fine in the whole project)
I don't understand why but the code in the client side returns an object with the claims property but this property doesn't have the admin claim I added in the cloud functions.
if I try to access idTokenResult.claims.admin like in the docs it logs me UNDEFINED.
Link to the docs - https://firebase.google.com/docs/auth/admin/custom-claims#node.js
Could you clarify to me what's wrong?
Thank you for your help!
The claims of a user are stored in their ID token, which is auto-refreshed by the SDK every hour.
Setting a custom claim to a user's profile from the Admin SDK does not force an auto-refresh of the ID token of the clients, so it may take up to an hour before the claim shows up in the clients. The documentation hints at this with:
Once the latest claims have propagated to a user's ID token, you can get them by retrieving the ID token: ...
This documentation could be more explicit though, so I'd recommend leaving feedback with the button at the bottom of that page.
To ensure a claim propagates to a client sooner, you should force the client to refresh its ID token (for example, by passing true to getIdTokenResult().

#azure/msal-node: Is there a way to log out / invalidate tokens?

I'm using the #azure/msal-node package in a node application to enable my users to log in using their AzureAD credentials. Logging in and acquiring session tokens works fine, but I cannot find a way to invalidate a session / log out a user - am I overlooking something obvious here?
Just for context, here's how I'm getting my tokens:
// msalConfig is my valid config object
const msalApp = new msal.ConfidentialClientApplication(msalConfig);
const authCodeUrlParameters = {
scopes: ['user.read'],
redirectUri: BASE_URL + '/msal-redirect'
};
try {
const authCodeResponse = await msalApp.getAuthCodeUrl(authCodeUrlParameters);
reply.redirect(authCodeResponse);
} catch (e) {
logError('auth code redirect error', e);
}
In the redirect handler, I'm doing this:
const tokenResponse = await msalApp.acquireTokenByCode({
code: request.query.code,
scopes: ['user.read'],
redirectUri: BASE_URL + '/msal-redirect'
});
and then I'm using that token to display the logged in user etc.
What I'm missing is something like msalApp.logout() - what am I not seeing here?
Unfortunately MSAL does not currently contain an msalApp.logout() API. Instead, you will have to manually implement the steps.
A logout operation will contain multiple steps:
Removing the account and the tokens from the msal application cache.
Redirecting to the AAD logout endpoint so the user logs out and AAD cookies are deleted.
If your webapp has a session, invalidating it.
For removing the account and tokens from the msal application cache, you can do something along the lines of:
const accounts = msalApp.getTokenCache().getAllAccounts();
// filter on the account that you want to delete from the cache.
// I take the first one here to keep the code sample short
const account = accounts[0];
msalApp.getTokenCache().removeAccount(account);
For logging out from AAD, you'll have to redirect the user to the Azure AD logout endpoint. The documentation here should explain how to craft this request.
In answer of sgonzalez, I can see the error in "accounts". This const is a Promisse:
const accounts = msalApp.getTokenCache().getAllAccounts(); // <- is a Promisse
// filter on the account that you want to delete from the cache.
// I take the first one here to keep the code sample short
const account = accounts[0];
msalApp.getTokenCache().removeAccount(account);
Correcting:
pca.getTokenCache().getAllAccounts().then((response) => {
const account = response[0];
pca.getTokenCache().removeAccount(account).then(() => {
res.sendStatus(200);
}).catch((error) => {
res.status(500).send({error});
});
}).catch((error) => {
res.status(500).send(error);
});
I don't know if this is the best way to implement it, but it worked for me.

Is there a way to prevent users from editing the local storage session?

I am creating a relational blog where I make use of ember_simple_auth:session to store the session like
{"authenticated":{"authenticator":"authenticator:devise","token":"rh2f9iy7EjJXESAM5koQ","email":"user#example.com","userId":1}}
However, on the developer tools on Chrome (and possibly on other browsers), it is quite easy to edit the email and userId in order to impersonate another user upon page reload.
EDIT #1
From the conversation with Joachim and Nikolaj, I now realized that the best way to tackle this problem is to probe the localStorage authenticity every time I need it (which is only on page reload) instead of attempting to prevent edits.
In order to validate authenticity, I create a promise that must be solved before the AccountSession can be used. The promise serverValidation() requests to create a token model with the current localStorage info, and when the server gets it, it validates the info and responds 200 with a simple user serialization with type as token if the information is legit. You can check more info on the Source Code.
Session Account
import Ember from 'ember';
const { inject: { service }, RSVP } = Ember;
export default Ember.Service.extend ({
session: service('session'),
store: service(),
serverValidation: false,
// Create a Promise to handle a server request that validates the current LocalStorage
// If valid, then set SessionAccount User.
loadCurrentUser() {
if (!Ember.isEmpty(this.get('session.data.authenticated.userId'))) {
this.serverValidation().then(() => {
return new RSVP.Promise((resolve, reject) => {
const userId = this.get('session.data.authenticated.userId');
// Get User to Session-Account Block
if(this.get('serverValidation') === true) {
return this.get('store').find('user', userId).then((user) => {
this.set('user', user);
resolve();
}).catch((reason) => {
console.log(reason.errors);
var possible404 = reason.errors.filterBy('status','404');
var possible500 = reason.errors.filterBy('status','500');
if(possible404.length !== 0) {
alert('404 | Sign In Not Found Error');
this.get('session').invalidate();
}
else if(possible500.length !== 0) {
alert('500 | Sign In Server Error');
this.get('session').invalidate();
}
reject();
});
}
else{
alert('Session for Server Validation failed! Logging out!');
this.get('session').invalidate();
resolve();
}
});
});
} else {
// Session is empty...
}
},
serverValidation() {
return new RSVP.Promise((resolve) => {
var tokenAuthentication = this.get('store').createRecord('token', {
id: this.get('session.data.authenticated.userId'),
email: this.get('session.data.authenticated.email'),
authenticity_token: this.get('session.data.authenticated.token'),
});
tokenAuthentication.save().then(() => {
this.set('serverValidation',true);
console.log('Server Validation complete with 200');
resolve();
}).catch((reason) => {
this.set('serverValidation',false);
resolve();
});
});
}
});
Token Controller
# Users Controller: JSON response through Active Model Serializers
class Api::V1::TokensController < ApiController
respond_to :json
def create
if token_by_id == token_by_token
if token_by_email == token_by_id
render json: token_by_id, serializer: TokenSerializer, status: 200
else
render json: {}, status: 404
end
else
render json: {}, status: 404
end
end
private
def token_by_id
User.find(user_params[:id])
end
def token_by_email
User.find_by(email: user_params[:email])
end
def token_by_token
User.find_by(authentication_token: user_params[:authenticity_token])
end
def user_params
ActiveModelSerializers::Deserialization.jsonapi_parse!(params.to_unsafe_h)
end
end
There is no way to prevent a user from editing the content of his local storage, session storage, or cookies.
But this should not worry you. The user is identified through the value of the token. The token is generated and sent to him by the authenticator when he logs in. To impersonate another user by editing the session data he would have to know that the other user is logged in, and know the token of that user.
Token is already signed on the server side, a standard JWT mechanism.
Having said that, there can be a couple of ways to check tempering in local storage:
Generate a token the way you already do.
Generate a random secret key to be kept on the server.
Generate a corresponding HMAC using this secret key.
Send the token + HMAC to the user.
When the user sends you this token, first check if HMAC is correct, if not then reject the token right away.
If HMAC is correct, validate the token the way you already do.
Another way:
Along with the token, a HMAC checksum too can be stored separately, and when sent back to the server by the client, check if checksum matches.

Resources