As a developer, how many times have you had a need to test different Microsoft 365 APIs? For me, this is a fairly frequent task. Most often it's SharePoint REST API or MS Graph. Every time you should think about the authentication part because all of those APIs are protected. With MS Graph explorer it's simple, however, you cannot test any other API except the MS Graph. Also, sometimes you have access to different customers environments or tenants and it's not that simple to easily switch between them, handle authentication, and so on.
I ended up testing everything in Postman using a generic approach, which works for any Azure AD-protected resource. This approach uses OAuth2 Auth code grant flow (or Resource Owner Password Credential flow, ROPC, also covered in this post), it stores tokens and automatically renews access tokens for a resource if this particular token is expired. This approach involves custom Azure AD app registration for Postman, Postman's environments feature, environment and collection-level variables, pre-request scripts.
Read further below to find out how I configured it. My approach is not a silver bullet, but at least it works for me. You can grab some ideas and adapt them to your requirements.
The idea
We're going to use Postman's collections as a container for all API requests per resource. For example, all SharePoint REST API queries will be in the corresponding "SharePoint REST API" collection. MS Graph in the other collection, etc. Inside the collection, you can use folders to further distinguish queries per customer or features if you wish. Every collection is also an authentication unit. It means that the authentication is configured at the collection level, all requests inherit authentication from the parent (i.e. from collection, this is a Postman feature).
At the Postman collection level, we also have a pre-request script, which checks whether an access token is expired or not. If yes, it renews it (using refresh token stored inside environment variables or just using ROPC flow directly). All child queries inherit pre-request script from parents, thus we don't care about authentication for individual queries.
As mentioned, I also use Postman's environments. Each environment is a container for tenant-specific values - tenant id, client\secret id, OAuth tokens. You can switch environments (think of it like switching tenants) and will be able to run queries against a different tenant without a hassle.
The implementation
Further on I'm going to configure everything using the SharePoint REST API as an example. However, as said, you can easily use this approach for any Azure AD-protected API.
Auth Code flow vs ROPC
Generally speaking, ROPC is not a recommended way of obtaining tokens because you have to provide a username and password in plain text during the request. It's not secure. Also, you might have additional difficulties if you use 2fa (you need an app password).
However, ROPC flow doesn't require user interaction at all thus a bit more convenient. If you have test or demo environments where the security is not that critical, you can use ROPC flow. In the below guide I will provide all information on how to use both of the auth flows. Actually, the difference is not that big.
Register an Azure AD app for Postman
Go to your target Azure AD and add a new app registration:
Just give it a name and that's it.
Then under authentication add a new Web platform. Use https://oauth.pstmn.io/v1/callback as a redirect Uri:
Under API permissions add necessary SharePoint API permissions. Under MS Graph also add offline_access permission, since it's needed for the refresh tokens:
Grant admin consent using the button above the permissions list.
Under Certificates and Secrets create a new secret and save it. Also, save Client Id and Tenant Id from the Overview screen, we'll need them later.
Configure Postman environment
Open Postman and add a new environment (an eye sign next to the environment dropdown in the top right corner):
Add below environment keys with values saved before (refresh token will be empty, will add it later):
For SharePoint REST API the resource is the host URL of your SharePoint tenant. For MS Graph it will be "https://graph.microsoft.com/".
For ROPC flow you should also add two variables with the corresponding values - UserName and Password. This is the user we will obtain an access token for.
Take a note of the "_sp" postfix. We're going to use it in order to distinguish tokens from different APIs.
Configure Postman collection
Create a new Postman collection with the name "SharePoint REST API".
Configure collection variables
Select a newly created collection and go to the Variables section. Add a new variable ResourceKey with the value, equals to the special postfix from environmental variables (in our case "sp"):
Configure authentication
Go to the Authorization section, select OAuth2 in the dropdown, in the access token field insert {{AccessToken_{{ResourceKey}}}}:
What is this {{AccessToken_{{ResourceKey}}}}? "{{key}}" is a special syntaxis in Postman. It dynamically replaces it with the value of a variable (either Environment or Collection). You can also create dynamic expression by combining the syntaxis. As a result {{AccessToken_{{ResourceKey}}}} -> {{AccessToken_sp}} -> [actual access token value from environment variable]. You do remember, that {{ResourceKey}} for the SharePoint REST API collection is "sp", right?
This is it for ROPC flow, for Auth code flow we need extra steps to manually obtain a refresh token. Read further below.
Scroll to the "Configure new token" section and update with below values:
- Auth Url - https://login.microsoftonline.com/{{TenantId}}//oauth2/v2.0/authorize
- Access Token Url - https://login.microsoftonline.com/{{TenantId}}//oauth2/v2.0/token
- Scope - {{Resource_{{ResourceKey}}}}.default offline_access
Click on Get New Access Token. The default browser will be opened. After successful authentication you will be presented with the below screen (enable popups for this page):
Click to open a postman:
In Postman you will see the access tokens dialog. Actually, we don't need an access token, but you need to copy the refresh token (scroll a bit to the bottom):
Now update RefreshToken_sp with the copied refresh token.
You see, for ROPC we don't need this manual step. Also, the refresh token expires in 90 days (if you don't use it).
Configure pre-request script
The pre-request script runs before the request is sent. Here you can do a lot of different things, like sending requests or updating variables.
What we're going to do is to check whether the access token is presented or not. If yes, whether it's expired or not. If the access token is OK, we return from the pre-request script. Otherwise, we generate a new access token using either a refresh token (auth code flow) or just a raw ROPC flow.
The code will be slightly different for different flows. Just copy and past it into the Pre-Request Script for the collection.
For auth code flow:
const key = pm.collectionVariables.get("ResourceKey");
if (!isAccessTokenExpired(key)) return;
if (!pm.environment.has(`RefreshToken_${key}`)) throw new Error(`Unable to find the refresh token with key "RefreshToken_${key}"`);
console.log(`Renewing the access token for the resource ${pm.environment.get(`Resource_${key}`)}`);
pm.sendRequest({
url: `https://login.microsoftonline.com/${pm.environment.get("TenantId")}/oauth2/v2.0/token`,
method: 'POST',
header: {
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded'
},
body: {
mode: 'urlencoded',
urlencoded: [
{ key: "client_id", value: pm.environment.get("ClientId") },
{ key: "scope", value: `${pm.environment.get(`Resource_${key}`)}.default offline_access` },
{ key: "redirect_uri", value: "https://oauth.pstmn.io/v1/callback" },
{ key: "grant_type", value: "refresh_token" },
{ key: "client_secret", value: pm.environment.get("ClientSecret") },
{ key: "refresh_token", value: pm.environment.get(`RefreshToken_${key}`) }
]
}
}, function (err, res) {
if (err) {
throw err;
}
const token = res.json();
if (token.error) {
throw new Error(token.error);
}
pm.environment.set(`AccessToken_${key}`, token.access_token);
pm.environment.set(`RefreshToken_${key}`, token.refresh_token);
console.log("The token was successfully renewed.");
});
function isAccessTokenExpired(key) {
if (!pm.environment.has(`AccessToken_${key}`)) {
return true;
}
const tokenString = pm.environment.get(`AccessToken_${key}`);
if (!tokenString) {
return true;
} else {
const accessToken = JSON.parse(atob(tokenString.split(".")[1]));
const expires = new Date(accessToken.exp * 1000);
const adjustment = 60 * 1000; // one minute
const expiresTime = expires.getTime() - adjustment;
const nowTime = (new Date()).getTime();
return nowTime > expiresTime;
}
}
How it works - we check that an access token is valid. If not, we use a refresh token to obtain a new access token. The refresh token is stored inside the "RefreshToken_sp" variable (we saved it on the previous step). The new access token and refresh token are then saved to the environment variable.
For the ROPC flow the code is somewhat similar:
const key = pm.collectionVariables.get("ResourceKey");
if (!isAccessTokenExpired(key)) return;
console.log(`Renewing the access token for the resource ${pm.environment.get(`Resource_${key}`)}`);
pm.sendRequest({
url: `https://login.microsoftonline.com/${pm.environment.get("TenantId")}/oauth2/v2.0/token`,
method: 'POST',
header: {
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded'
},
body: {
mode: 'urlencoded',
urlencoded: [
{ key: "client_id", value: pm.environment.get("ClientId") },
{ key: "scope", value: `${pm.environment.get(`Resource_${key}`)}.default offline_access` },
{ key: "grant_type", value: "password" },
{ key: "client_secret", value: pm.environment.get("ClientSecret") },
{ key: "username", value: pm.environment.get("UserName") },
{ key: "password", value: pm.environment.get("Password") }
]
}
}, function (err, res) {
if (err) {
throw err;
}
const token = res.json();
if (token.error) {
throw new Error(token.error);
}
pm.environment.set(`AccessToken_${key}`, token.access_token);
pm.environment.set(`RefreshToken_${key}`, token.refresh_token);
console.log("The token was successfully renewed.");
});
function isAccessTokenExpired(key) {
if (!pm.environment.has(`AccessToken_${key}`)) {
return true;
}
const tokenString = pm.environment.get(`AccessToken_${key}`);
if (!tokenString) {
return true;
} else {
const accessToken = JSON.parse(atob(tokenString.split(".")[1]));
const expires = new Date(accessToken.exp * 1000);
const adjustment = 60 * 1000; // one minute
const expiresTime = expires.getTime() - adjustment;
const nowTime = (new Date()).getTime();
return nowTime > expiresTime;
}
}
The only difference is that this time we generate a new access token using ROPC flow directly. Thus we don't need any refresh tokens for this type of auth flow. Though we need a username and password.
This is it! Now you can add new requests under SharePoint REST collection and the authentication will be handled automatically.
How to add a new service
If you need to test a new service, say MS Graph, then you should do the following:
- Update the Postman AD app registration with needed permissions for MS Graph.
- Duplicate Postman's SharePoint REST collection (so you have a copy of all configurations like pre-request scripts and variables)
- Under collection variables change the value for ResourceKey to, say "graph"
- Under Authorization tab update access token and set to be {{AccessToken_{{ResourceKey}}}}
- If you use Auth code flow, you also have to generate a new refresh token for MS Graph using the "Get New Access Token" button.
- Update environment and add new key-values -
- Resource_graph=https://graph.microsoft.com/
- RefreshToken_graph=[refresh token from step 4]
Now you can write queries for MS Graph as well under the newly created collection.
How to switch environment
If you have multiple tenants, you should create an environment for every tenant and fill in all environment variables - TenantId, ClientId, ClientSecret, etc.
Then just by switching dropdown in Postman, you can easily test your REST queries across different tenants.
Title image credits - Design vector created by freepik - www.freepik.com