126 lines
3.1 KiB
JavaScript
126 lines
3.1 KiB
JavaScript
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;
|