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