Last active
April 5, 2024 05:37
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
It turns out that in Postman's
pm.sendRequest({..}, (err, res) => {..})
sometimes when there is an error (e.g. 400) theerr
can benull
while theres
contains the body. To cover this scenario, I added the following code in mysendTokenRequest
to be safe: