2021-12-07 13:18:08 -05:00

124 lines
3.6 KiB
JavaScript

/* eslint-disable camelcase */
const { inspect } = require('util');
const { RPError, OPError } = require('./errors');
const instance = require('./helpers/weak_cache');
const now = require('./helpers/unix_timestamp');
const { authenticatedPost } = require('./helpers/client');
const processResponse = require('./helpers/process_response');
const TokenSet = require('./token_set');
class DeviceFlowHandle {
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);
}
instance(this).expires_at = now() + response.expires_in;
instance(this).client = client;
instance(this).DPoP = DPoP;
instance(this).maxAge = maxAge;
instance(this).exchangeBody = exchangeBody;
instance(this).clientAssertionPayload = clientAssertionPayload;
instance(this).response = response;
instance(this).interval = response.interval * 1000 || 5000;
}
abort() {
instance(this).aborted = true;
}
async poll({ signal } = {}) {
if ((signal && signal.aborted) || instance(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, instance(this).interval));
const response = await authenticatedPost.call(
instance(this).client,
'token',
{
form: {
...instance(this).exchangeBody,
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
device_code: this.device_code,
},
responseType: 'json',
},
{ clientAssertionPayload: instance(this).clientAssertionPayload, DPoP: instance(this).DPoP },
);
let responseBody;
try {
responseBody = processResponse(response);
} catch (err) {
switch (err instanceof OPError && err.error) {
case 'slow_down':
instance(this).interval += 5000;
case 'authorization_pending': // eslint-disable-line no-fallthrough
return this.poll({ signal });
default:
throw err;
}
}
const tokenset = new TokenSet(responseBody);
if ('id_token' in tokenset) {
await instance(this).client.decryptIdToken(tokenset);
await instance(this).client.validateIdToken(tokenset, undefined, 'token', instance(this).maxAge);
}
return tokenset;
}
get device_code() {
return instance(this).response.device_code;
}
get user_code() {
return instance(this).response.user_code;
}
get verification_uri() {
return instance(this).response.verification_uri;
}
get verification_uri_complete() {
return instance(this).response.verification_uri_complete;
}
get expires_in() {
return Math.max.apply(null, [instance(this).expires_at - now(), 0]);
}
expired() {
return this.expires_in === 0;
}
/* istanbul ignore next */
[inspect.custom]() {
return `${this.constructor.name} ${inspect(instance(this).response, {
depth: Infinity,
colors: process.stdout.isTTY,
compact: false,
sorted: true,
})}`;
}
}
module.exports = DeviceFlowHandle;