170 lines
5.9 KiB
JavaScript
170 lines
5.9 KiB
JavaScript
const { http, https } = require('follow-redirects');
|
|
const querystring = require('querystring');
|
|
const services = require('./services');
|
|
const crypto = require('crypto');
|
|
|
|
class OAuth2 {
|
|
|
|
constructor(app, opts, name) {
|
|
this.app = app;
|
|
this.name = name;
|
|
this.opts = opts;
|
|
|
|
if (opts.service) {
|
|
Object.assign(this.opts, services[opts.service]);
|
|
}
|
|
|
|
this.access_token = opts.access_token || app.getSession(`${name}_access_token`);
|
|
this.refresh_token = opts.refresh_token || app.getSession(`${name}_refresh_token`);
|
|
|
|
if (this.access_token) {
|
|
let expires = app.getSession(`${name}_expires`);
|
|
|
|
if (expires && expires < expires_in(-10)) {
|
|
this.access_token = null;
|
|
|
|
app.removeSession(`${name}_access_token`);
|
|
app.removeSession(`${name}_expires`);
|
|
|
|
if (this.refresh_token) {
|
|
this.refreshToken(this.refresh_token);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async init() {
|
|
if (this.opts.jwt_bearer && !this.access_token) {
|
|
const assertion = this.app.getJSONWebToken(this.opts.jwt_bearer);
|
|
let response = await this.grant('urn:ietf:params:oauth:grant-type:jwt-bearer', { assertion });
|
|
this.access_token = response.access_token;
|
|
}
|
|
|
|
if (this.opts.client_credentials && !this.access_token) {
|
|
let response = await this.grant('client_credentials');
|
|
this.access_token = response.access_token;
|
|
}
|
|
}
|
|
|
|
async authorize(scopes = [], params = {}) {
|
|
let query = this.app.req.query;
|
|
|
|
if (query.state && query.state == this.app.getSession(`${this.name}_state`)) {
|
|
this.app.removeSession(`${this.name}_state`);
|
|
|
|
if (query.error) {
|
|
throw new Error(query.error_message || query.error);
|
|
}
|
|
|
|
if (query.code) {
|
|
let params = {
|
|
redirect_uri: redirect_uri(this.app.req),
|
|
code: query.code
|
|
};
|
|
|
|
if (this.app.getSession(`${this.name}_code_verifier`)) {
|
|
params.code_verifier = this.app.getSession(`${this.name}_code_verifier`);
|
|
}
|
|
|
|
return this.grant('authorization_code', params);
|
|
}
|
|
}
|
|
|
|
if (this.opts.pkce || params.code_verifier) {
|
|
const code_verifier = params.code_verifier || crypto.randomBytes(40).toString('hex');
|
|
this.app.setSession(`${this.name}_code_verifier`, code_verifier);
|
|
params.code_challenge_method = 'S256';
|
|
params.code_challenge = crypto.createHash('sha256').update(code_verifier).digest('base64').replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
|
|
}
|
|
|
|
params = Object.assign({}, {
|
|
response_type: 'code',
|
|
client_id: this.opts.client_id,
|
|
scope: scopes.join(this.opts.scope_separator),
|
|
redirect_uri: redirect_uri(this.app.req),
|
|
state: generate_state()
|
|
}, this.opts.params, params);
|
|
|
|
this.app.setSession(`${this.name}_state`, params.state);
|
|
|
|
this.app.res.redirect(build_url(this.opts.auth_endpoint, params));
|
|
}
|
|
|
|
async refreshToken(refresh_token) {
|
|
return this.grant('refresh_token', { refresh_token });
|
|
}
|
|
|
|
async grant(type, params) {
|
|
const endpoint = new URL(this.opts.token_endpoint);
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const req = (endpoint.protocol == 'https:' ? https : http).request(endpoint, {
|
|
method: 'POST'
|
|
}, res => {
|
|
let body = '';
|
|
res.setEncoding('utf8');
|
|
res.on('data', chunk => body += chunk);
|
|
res.on('end', () => {
|
|
if (res.statusCode >= 400) {
|
|
return reject(new Error(`Http status code ${res.statusCode}. ${body}`));
|
|
}
|
|
|
|
try {
|
|
body = JSON.parse(body);
|
|
} catch (e) { }
|
|
|
|
if (!body.access_token) {
|
|
return reject(new Error(`Http response has no access_token. ${JSON.stringify(body)}`))
|
|
}
|
|
|
|
this.app.setSession(`${this.name}_access_token`, body.access_token);
|
|
this.access_token = body.access_token;
|
|
|
|
if (body.expires_in) {
|
|
this.app.setSession(`${this.name}_expires`, expires_in(body.expires_in));
|
|
}
|
|
|
|
if (body.refresh_token) {
|
|
this.app.setSession(`${this.name}_refresh_token`, body.refresh_token);
|
|
this.refresh_token = body.refresh_token;
|
|
}
|
|
|
|
resolve(body);
|
|
});
|
|
});
|
|
|
|
const body = querystring.stringify(Object.assign({
|
|
grant_type: type,
|
|
client_id: this.opts.client_id,
|
|
client_secret: this.opts.client_secret
|
|
}, params));
|
|
|
|
req.on('error', reject);
|
|
req.setHeader('Content-Type', 'application/x-www-form-urlencoded');
|
|
req.setHeader('Content-Length', body.length); // required by azure endpoint
|
|
req.write(body);
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
}
|
|
|
|
function build_url(url, params) {
|
|
return url + (url.indexOf('?') != -1 ? '&' : '?') + querystring.stringify(params);
|
|
}
|
|
|
|
function redirect_uri(req) {
|
|
let hasProxy = !!req.get('x-forwarded-host');
|
|
return `${req.protocol}://${hasProxy ? req.hostname : req.get('host')}${req.path}`;
|
|
}
|
|
|
|
function expires_in(s) {
|
|
return ~~(Date.now() / 1000) + s;
|
|
}
|
|
|
|
function generate_state() {
|
|
const { v4: uuidv4 } = require('uuid');
|
|
return uuidv4();
|
|
}
|
|
|
|
module.exports = OAuth2; |