Skip to content

Instantly share code, notes, and snippets.

@rezanid
Last active April 5, 2024 05:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rezanid/b149cc77c48afc678de719a6e8133f54 to your computer and use it in GitHub Desktop.
Save rezanid/b149cc77c48afc678de719a6e8133f54 to your computer and use it in GitHub Desktop.
Postman Pre-Request script to authenticate and refresh authentication token using OAuth2 device flow
// Environment Variables:
// url: <your-resource> example: https://myfancyapp.crm4.dynamics.com
// clientid: <user-clientid-from-appreg> example: 1950a258-227b-4e31-a9cf-717495945fc2
// To know how to use this script, please read the following blog post:
// https://bycode.dev/2024/04/04/automatically-authenticate-in-postman-with-pre-request-scripts/
const utils = {
auth: {
message: "",
async refreshAuth() {
console.log(pm.environment.get("clientid"));
const tokenExpiry = new Date(pm.collectionVariables.get("tokenexpiresat") || 0);
if (!tokenExpiry || tokenExpiry <= new Date()) {
console.info("Either you are not authenticated or the token has expired.");
const refreshToken = pm.collectionVariables.get("refresh_token");
if (refreshToken) {
try {
await this.refreshToken(refreshToken);
} catch (error) {
console.error("Token refresh failed.", error);
}
} else {
await this.authenticateWithDeviceCode();
}
} else {
console.info("Token is still valid.");
}
},
async refreshToken(refreshToken) {
const response = await this.sendTokenRequest({
"client_id": pm.environment.get("clientid"),
"scope": `${pm.environment.get("url")}/.default`,
"refresh_token": refreshToken,
"grant_type": "refresh_token"
});
if (response) {
console.log("Token refresh succeeded");
this.updateTokens(response);
}
},
async authenticateWithDeviceCode() {
try {
const deviceCodeResponse = await this.requestDeviceCode();
this.message = deviceCodeResponse.message;
console.info(`Please go to ${deviceCodeResponse.verification_uri} and enter the code ${deviceCodeResponse.user_code} to authenticate.`);
await this.pollToken(deviceCodeResponse, 5);
} catch (error) {
console.error("Authentication with device code failed", error);
}
},
async requestDeviceCode() {
return new Promise((resolve, reject) => {
pm.sendRequest({
url: "https://login.microsoftonline.com/organizations/oauth2/v2.0/devicecode",
method: "POST",
header: { "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("url")}/.default offline_access` }
]
}
}, (err, res) => {
if (err) {
reject(err);
} else {
const responseJson = res.json();
if (responseJson.error) {
reject(responseJson)
} else {
resolve(responseJson);
}
}
});
});
},
async pollToken({ device_code, interval }) {
return new Promise((resolve, reject) => {
const poll = setInterval(() => {
console.log("polling...");
this.sendTokenRequest({
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
"device_code": device_code,
"client_id": pm.environment.get("clientid")
}).then(response => {
console.info("Authentication successful");
this.updateTokens(response);
clearInterval(poll);
resolve(response);
}).catch(err => {
if (err.error !== "authorization_pending") {
console.error("Authentication failed or canceled", err);
clearInterval(poll);
reject(err);
}
});
}, interval * 1000); // `interval` is the polling interval in seconds suggested by the authorization server
});
},
async sendTokenRequest(body) {
// This function abstracts the token request logic
return new Promise((resolve, reject) => {
pm.sendRequest({
url: "https://login.microsoftonline.com/organizations/oauth2/v2.0/token",
method: "POST",
header: {"Content-Type": "application/x-www-form-urlencoded"},
body: { mode: "urlencoded", urlencoded: Object.entries(body).map(([key, value]) => ({ key, value })) }
}, (err, res) => {
if (err) {
console.error("Request failed", err);
reject(err);
} else {
const responseJson = res.json();
if (responseJson.error) {
reject(responseJson)
} else {
resolve(responseJson);
}
}
});
});
},
updateTokens({ expires_in, access_token, refresh_token }) {
const tokenExpiry = new Date(Date.now() + expires_in * 1000);
pm.collectionVariables.set("tokenexpiresat", tokenExpiry.toString());
pm.collectionVariables.set("accesstoken", access_token);
pm.collectionVariables.set("refresh_token", refresh_token);
}
}
};
if (pm.collectionVariables.get("autoAuth") === "true") {
utils.auth.refreshAuth(pm).then(() => console.info("Authentication valid")).catch(console.error);
}
@rezanid
Copy link
Author

rezanid commented Apr 4, 2024

It turns out that in Postman's pm.sendRequest({..}, (err, res) => {..}) sometimes when there is an error (e.g. 400) the err can be null while the res contains the body. To cover this scenario, I added the following code in my sendTokenRequest to be safe:

const responseJson = res.json();
if (responseJson.error) {
    reject(responseJson)
} else {
    resolve(responseJson);
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment