auth.js 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. 'use strict'
  2. const fs = require('fs')
  3. const npa = require('npm-package-arg')
  4. const { URL } = require('url')
  5. // Find the longest registry key that is used for some kind of auth
  6. // in the options. Returns the registry key and the auth config.
  7. const regFromURI = (uri, opts) => {
  8. const parsed = new URL(uri)
  9. // try to find a config key indicating we have auth for this registry
  10. // can be one of :_authToken, :_auth, :_password and :username, or
  11. // :certfile and :keyfile
  12. // We walk up the "path" until we're left with just //<host>[:<port>],
  13. // stopping when we reach '//'.
  14. let regKey = `//${parsed.host}${parsed.pathname}`
  15. while (regKey.length > '//'.length) {
  16. const authKey = hasAuth(regKey, opts)
  17. // got some auth for this URI
  18. if (authKey) {
  19. return { regKey, authKey }
  20. }
  21. // can be either //host/some/path/:_auth or //host/some/path:_auth
  22. // walk up by removing EITHER what's after the slash OR the slash itself
  23. regKey = regKey.replace(/([^/]+|\/)$/, '')
  24. }
  25. return { regKey: false, authKey: null }
  26. }
  27. // Not only do we want to know if there is auth, but if we are calling `npm
  28. // logout` we want to know what config value specifically provided it. This is
  29. // so we can look up where the config came from to delete it (i.e. user vs
  30. // project)
  31. const hasAuth = (regKey, opts) => {
  32. if (opts[`${regKey}:_authToken`]) {
  33. return '_authToken'
  34. }
  35. if (opts[`${regKey}:_auth`]) {
  36. return '_auth'
  37. }
  38. if (opts[`${regKey}:username`] && opts[`${regKey}:_password`]) {
  39. // 'password' can be inferred to also be present
  40. return 'username'
  41. }
  42. if (opts[`${regKey}:certfile`] && opts[`${regKey}:keyfile`]) {
  43. // 'keyfile' can be inferred to also be present
  44. return 'certfile'
  45. }
  46. return false
  47. }
  48. const sameHost = (a, b) => {
  49. const parsedA = new URL(a)
  50. const parsedB = new URL(b)
  51. return parsedA.host === parsedB.host
  52. }
  53. const getRegistry = opts => {
  54. const { spec } = opts
  55. const { scope: specScope, subSpec } = spec ? npa(spec) : {}
  56. const subSpecScope = subSpec && subSpec.scope
  57. const scope = subSpec ? subSpecScope : specScope
  58. const scopeReg = scope && opts[`${scope}:registry`]
  59. return scopeReg || opts.registry
  60. }
  61. const maybeReadFile = file => {
  62. try {
  63. return fs.readFileSync(file, 'utf8')
  64. } catch (er) {
  65. if (er.code !== 'ENOENT') {
  66. throw er
  67. }
  68. return null
  69. }
  70. }
  71. const getAuth = (uri, opts = {}) => {
  72. const { forceAuth } = opts
  73. if (!uri) {
  74. throw new Error('URI is required')
  75. }
  76. const { regKey, authKey } = regFromURI(uri, forceAuth || opts)
  77. // we are only allowed to use what's in forceAuth if specified
  78. if (forceAuth && !regKey) {
  79. return new Auth({
  80. // if we force auth we don't want to refer back to anything in config
  81. regKey: false,
  82. authKey: null,
  83. scopeAuthKey: null,
  84. token: forceAuth._authToken || forceAuth.token,
  85. username: forceAuth.username,
  86. password: forceAuth._password || forceAuth.password,
  87. auth: forceAuth._auth || forceAuth.auth,
  88. certfile: forceAuth.certfile,
  89. keyfile: forceAuth.keyfile,
  90. })
  91. }
  92. // no auth for this URI, but might have it for the registry
  93. if (!regKey) {
  94. const registry = getRegistry(opts)
  95. if (registry && uri !== registry && sameHost(uri, registry)) {
  96. return getAuth(registry, opts)
  97. } else if (registry !== opts.registry) {
  98. // If making a tarball request to a different base URI than the
  99. // registry where we logged in, but the same auth SHOULD be sent
  100. // to that artifact host, then we track where it was coming in from,
  101. // and warn the user if we get a 4xx error on it.
  102. const { regKey: scopeAuthKey, authKey: _authKey } = regFromURI(registry, opts)
  103. return new Auth({ scopeAuthKey, regKey: scopeAuthKey, authKey: _authKey })
  104. }
  105. }
  106. const {
  107. [`${regKey}:_authToken`]: token,
  108. [`${regKey}:username`]: username,
  109. [`${regKey}:_password`]: password,
  110. [`${regKey}:_auth`]: auth,
  111. [`${regKey}:certfile`]: certfile,
  112. [`${regKey}:keyfile`]: keyfile,
  113. } = opts
  114. return new Auth({
  115. scopeAuthKey: null,
  116. regKey,
  117. authKey,
  118. token,
  119. auth,
  120. username,
  121. password,
  122. certfile,
  123. keyfile,
  124. })
  125. }
  126. class Auth {
  127. constructor ({
  128. token,
  129. auth,
  130. username,
  131. password,
  132. scopeAuthKey,
  133. certfile,
  134. keyfile,
  135. regKey,
  136. authKey,
  137. }) {
  138. // same as regKey but only present for scoped auth. Should have been named scopeRegKey
  139. this.scopeAuthKey = scopeAuthKey
  140. // `${regKey}:${authKey}` will get you back to the auth config that gave us auth
  141. this.regKey = regKey
  142. this.authKey = authKey
  143. this.token = null
  144. this.auth = null
  145. this.isBasicAuth = false
  146. this.cert = null
  147. this.key = null
  148. if (token) {
  149. this.token = token
  150. } else if (auth) {
  151. this.auth = auth
  152. } else if (username && password) {
  153. const p = Buffer.from(password, 'base64').toString('utf8')
  154. this.auth = Buffer.from(`${username}:${p}`, 'utf8').toString('base64')
  155. this.isBasicAuth = true
  156. }
  157. // mTLS may be used in conjunction with another auth method above
  158. if (certfile && keyfile) {
  159. const cert = maybeReadFile(certfile, 'utf-8')
  160. const key = maybeReadFile(keyfile, 'utf-8')
  161. if (cert && key) {
  162. this.cert = cert
  163. this.key = key
  164. }
  165. }
  166. }
  167. }
  168. module.exports = getAuth