generated from erangel1/generic-template
initial commit. phase 1 complete
This commit is contained in:
+125
@@ -0,0 +1,125 @@
|
||||
const { inspect } = require('util');
|
||||
|
||||
const { RPError, OPError } = require('./errors');
|
||||
const now = require('./helpers/unix_timestamp');
|
||||
|
||||
class DeviceFlowHandle {
|
||||
#aborted;
|
||||
#client;
|
||||
#clientAssertionPayload;
|
||||
#DPoP;
|
||||
#exchangeBody;
|
||||
#expires_at;
|
||||
#interval;
|
||||
#maxAge;
|
||||
#response;
|
||||
constructor({ client, exchangeBody, clientAssertionPayload, response, maxAge, DPoP }) {
|
||||
['verification_uri', 'user_code', 'device_code'].forEach((prop) => {
|
||||
if (typeof response[prop] !== 'string' || !response[prop]) {
|
||||
throw new RPError(
|
||||
`expected ${prop} string to be returned by Device Authorization Response, got %j`,
|
||||
response[prop],
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (!Number.isSafeInteger(response.expires_in)) {
|
||||
throw new RPError(
|
||||
'expected expires_in number to be returned by Device Authorization Response, got %j',
|
||||
response.expires_in,
|
||||
);
|
||||
}
|
||||
|
||||
this.#expires_at = now() + response.expires_in;
|
||||
this.#client = client;
|
||||
this.#DPoP = DPoP;
|
||||
this.#maxAge = maxAge;
|
||||
this.#exchangeBody = exchangeBody;
|
||||
this.#clientAssertionPayload = clientAssertionPayload;
|
||||
this.#response = response;
|
||||
this.#interval = response.interval * 1000 || 5000;
|
||||
}
|
||||
|
||||
abort() {
|
||||
this.#aborted = true;
|
||||
}
|
||||
|
||||
async poll({ signal } = {}) {
|
||||
if ((signal && signal.aborted) || this.#aborted) {
|
||||
throw new RPError('polling aborted');
|
||||
}
|
||||
|
||||
if (this.expired()) {
|
||||
throw new RPError(
|
||||
'the device code %j has expired and the device authorization session has concluded',
|
||||
this.device_code,
|
||||
);
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, this.#interval));
|
||||
|
||||
let tokenset;
|
||||
try {
|
||||
tokenset = await this.#client.grant(
|
||||
{
|
||||
...this.#exchangeBody,
|
||||
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
||||
device_code: this.device_code,
|
||||
},
|
||||
{ clientAssertionPayload: this.#clientAssertionPayload, DPoP: this.#DPoP },
|
||||
);
|
||||
} catch (err) {
|
||||
switch (err instanceof OPError && err.error) {
|
||||
case 'slow_down':
|
||||
this.#interval += 5000;
|
||||
case 'authorization_pending':
|
||||
return this.poll({ signal });
|
||||
default:
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
if ('id_token' in tokenset) {
|
||||
await this.#client.decryptIdToken(tokenset);
|
||||
await this.#client.validateIdToken(tokenset, undefined, 'token', this.#maxAge);
|
||||
}
|
||||
|
||||
return tokenset;
|
||||
}
|
||||
|
||||
get device_code() {
|
||||
return this.#response.device_code;
|
||||
}
|
||||
|
||||
get user_code() {
|
||||
return this.#response.user_code;
|
||||
}
|
||||
|
||||
get verification_uri() {
|
||||
return this.#response.verification_uri;
|
||||
}
|
||||
|
||||
get verification_uri_complete() {
|
||||
return this.#response.verification_uri_complete;
|
||||
}
|
||||
|
||||
get expires_in() {
|
||||
return Math.max.apply(null, [this.#expires_at - now(), 0]);
|
||||
}
|
||||
|
||||
expired() {
|
||||
return this.expires_in === 0;
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
[inspect.custom]() {
|
||||
return `${this.constructor.name} ${inspect(this.#response, {
|
||||
depth: Infinity,
|
||||
colors: process.stdout.isTTY,
|
||||
compact: false,
|
||||
sorted: true,
|
||||
})}`;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DeviceFlowHandle;
|
||||
Reference in New Issue
Block a user