You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

170 regels
5.9 KiB

  1. const { http, https } = require('follow-redirects');
  2. const querystring = require('querystring');
  3. const services = require('./services');
  4. const crypto = require('crypto');
  5. class OAuth2 {
  6. constructor(app, opts, name) {
  7. this.app = app;
  8. this.name = name;
  9. this.opts = opts;
  10. if (opts.service) {
  11. Object.assign(this.opts, services[opts.service]);
  12. }
  13. this.access_token = opts.access_token || app.getSession(`${name}_access_token`);
  14. this.refresh_token = opts.refresh_token || app.getSession(`${name}_refresh_token`);
  15. if (this.access_token) {
  16. let expires = app.getSession(`${name}_expires`);
  17. if (expires && expires < expires_in(-10)) {
  18. this.access_token = null;
  19. app.removeSession(`${name}_access_token`);
  20. app.removeSession(`${name}_expires`);
  21. if (this.refresh_token) {
  22. this.refreshToken(this.refresh_token);
  23. }
  24. }
  25. }
  26. }
  27. async init() {
  28. if (this.opts.jwt_bearer && !this.access_token) {
  29. const assertion = this.app.getJSONWebToken(this.opts.jwt_bearer);
  30. let response = await this.grant('urn:ietf:params:oauth:grant-type:jwt-bearer', { assertion });
  31. this.access_token = response.access_token;
  32. }
  33. if (this.opts.client_credentials && !this.access_token) {
  34. let response = await this.grant('client_credentials');
  35. this.access_token = response.access_token;
  36. }
  37. }
  38. async authorize(scopes = [], params = {}) {
  39. let query = this.app.req.query;
  40. if (query.state && query.state == this.app.getSession(`${this.name}_state`)) {
  41. this.app.removeSession(`${this.name}_state`);
  42. if (query.error) {
  43. throw new Error(query.error_message || query.error);
  44. }
  45. if (query.code) {
  46. let params = {
  47. redirect_uri: redirect_uri(this.app.req),
  48. code: query.code
  49. };
  50. if (this.app.getSession(`${this.name}_code_verifier`)) {
  51. params.code_verifier = this.app.getSession(`${this.name}_code_verifier`);
  52. }
  53. return this.grant('authorization_code', params);
  54. }
  55. }
  56. if (this.opts.pkce || params.code_verifier) {
  57. const code_verifier = params.code_verifier || crypto.randomBytes(40).toString('hex');
  58. this.app.setSession(`${this.name}_code_verifier`, code_verifier);
  59. params.code_challenge_method = 'S256';
  60. params.code_challenge = crypto.createHash('sha256').update(code_verifier).digest('base64').replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
  61. }
  62. params = Object.assign({}, {
  63. response_type: 'code',
  64. client_id: this.opts.client_id,
  65. scope: scopes.join(this.opts.scope_separator),
  66. redirect_uri: redirect_uri(this.app.req),
  67. state: generate_state()
  68. }, this.opts.params, params);
  69. this.app.setSession(`${this.name}_state`, params.state);
  70. this.app.res.redirect(build_url(this.opts.auth_endpoint, params));
  71. }
  72. async refreshToken(refresh_token) {
  73. return this.grant('refresh_token', { refresh_token });
  74. }
  75. async grant(type, params) {
  76. const endpoint = new URL(this.opts.token_endpoint);
  77. return new Promise((resolve, reject) => {
  78. const req = (endpoint.protocol == 'https:' ? https : http).request(endpoint, {
  79. method: 'POST'
  80. }, res => {
  81. let body = '';
  82. res.setEncoding('utf8');
  83. res.on('data', chunk => body += chunk);
  84. res.on('end', () => {
  85. if (res.statusCode >= 400) {
  86. return reject(new Error(`Http status code ${res.statusCode}. ${body}`));
  87. }
  88. try {
  89. body = JSON.parse(body);
  90. } catch (e) { }
  91. if (!body.access_token) {
  92. return reject(new Error(`Http response has no access_token. ${JSON.stringify(body)}`))
  93. }
  94. this.app.setSession(`${this.name}_access_token`, body.access_token);
  95. this.access_token = body.access_token;
  96. if (body.expires_in) {
  97. this.app.setSession(`${this.name}_expires`, expires_in(body.expires_in));
  98. }
  99. if (body.refresh_token) {
  100. this.app.setSession(`${this.name}_refresh_token`, body.refresh_token);
  101. this.refresh_token = body.refresh_token;
  102. }
  103. resolve(body);
  104. });
  105. });
  106. const body = querystring.stringify(Object.assign({
  107. grant_type: type,
  108. client_id: this.opts.client_id,
  109. client_secret: this.opts.client_secret
  110. }, params));
  111. req.on('error', reject);
  112. req.setHeader('Content-Type', 'application/x-www-form-urlencoded');
  113. req.setHeader('Content-Length', body.length); // required by azure endpoint
  114. req.write(body);
  115. req.end();
  116. });
  117. }
  118. }
  119. function build_url(url, params) {
  120. return url + (url.indexOf('?') != -1 ? '&' : '?') + querystring.stringify(params);
  121. }
  122. function redirect_uri(req) {
  123. let hasProxy = !!req.get('x-forwarded-host');
  124. return `${req.protocol}://${hasProxy ? req.hostname : req.get('host')}${req.path}`;
  125. }
  126. function expires_in(s) {
  127. return ~~(Date.now() / 1000) + s;
  128. }
  129. function generate_state() {
  130. const { v4: uuidv4 } = require('uuid');
  131. return uuidv4();
  132. }
  133. module.exports = OAuth2;