summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKitson Kelly <me@kitsonkelly.com>2021-02-16 13:50:27 +1100
committerGitHub <noreply@github.com>2021-02-16 13:50:27 +1100
commit879897ada6247e1bb19905b12e5d2fd99965089e (patch)
tree6ca174ec6b6408cfd21746ca1703d188782c4094
parentccd6ee5c2394418c078f1a1be9e5cc1012829cbc (diff)
feat(cli): support auth tokens for accessing private modules (#9508)
Closes #5239
-rw-r--r--cli/auth_tokens.rs144
-rw-r--r--cli/file_fetcher.rs24
-rw-r--r--cli/flags.rs12
-rw-r--r--cli/http_util.rs135
-rw-r--r--cli/main.rs1
-rw-r--r--cli/tests/integration_tests.rs36
-rw-r--r--docs/images/private-github-new-token.pngbin0 -> 67173 bytes
-rw-r--r--docs/images/private-github-token-display.pngbin0 -> 32634 bytes
-rw-r--r--docs/images/private-pat.pngbin0 -> 11359 bytes
-rw-r--r--docs/linking_to_external_code/private.md75
-rw-r--r--docs/toc.json1
-rw-r--r--test_util/src/lib.rs36
12 files changed, 430 insertions, 34 deletions
diff --git a/cli/auth_tokens.rs b/cli/auth_tokens.rs
new file mode 100644
index 000000000..f52f564e1
--- /dev/null
+++ b/cli/auth_tokens.rs
@@ -0,0 +1,144 @@
+// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
+
+use deno_core::ModuleSpecifier;
+use std::fmt;
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct AuthToken {
+ host: String,
+ token: String,
+}
+
+impl fmt::Display for AuthToken {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "Bearer {}", self.token)
+ }
+}
+
+/// A structure which contains bearer tokens that can be used when sending
+/// requests to websites, intended to authorize access to private resources
+/// such as remote modules.
+#[derive(Debug, Clone)]
+pub struct AuthTokens(Vec<AuthToken>);
+
+impl AuthTokens {
+ /// Create a new set of tokens based on the provided string. It is intended
+ /// that the string be the value of an environment variable and the string is
+ /// parsed for token values. The string is expected to be a semi-colon
+ /// separated string, where each value is `{token}@{hostname}`.
+ pub fn new(maybe_tokens_str: Option<String>) -> Self {
+ let mut tokens = Vec::new();
+ if let Some(tokens_str) = maybe_tokens_str {
+ for token_str in tokens_str.split(';') {
+ if token_str.contains('@') {
+ let pair: Vec<&str> = token_str.rsplitn(2, '@').collect();
+ let token = pair[1].to_string();
+ let host = pair[0].to_lowercase();
+ tokens.push(AuthToken { host, token });
+ } else {
+ error!("Badly formed auth token discarded.");
+ }
+ }
+ debug!("Parsed {} auth token(s).", tokens.len());
+ }
+
+ Self(tokens)
+ }
+
+ /// Attempt to match the provided specifier to the tokens in the set. The
+ /// matching occurs from the right of the hostname plus port, irrespective of
+ /// scheme. For example `https://www.deno.land:8080/` would match a token
+ /// with a host value of `deno.land:8080` but not match `www.deno.land`. The
+ /// matching is case insensitive.
+ pub fn get(&self, specifier: &ModuleSpecifier) -> Option<AuthToken> {
+ self.0.iter().find_map(|t| {
+ let url = specifier.as_url();
+ let hostname = if let Some(port) = url.port() {
+ format!("{}:{}", url.host_str()?, port)
+ } else {
+ url.host_str()?.to_string()
+ };
+ if hostname.to_lowercase().ends_with(&t.host) {
+ Some(t.clone())
+ } else {
+ None
+ }
+ })
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_auth_token() {
+ let auth_tokens = AuthTokens::new(Some("abc123@deno.land".to_string()));
+ let fixture =
+ ModuleSpecifier::resolve_url("https://deno.land/x/mod.ts").unwrap();
+ assert_eq!(
+ auth_tokens.get(&fixture).unwrap().to_string(),
+ "Bearer abc123"
+ );
+ let fixture =
+ ModuleSpecifier::resolve_url("https://www.deno.land/x/mod.ts").unwrap();
+ assert_eq!(
+ auth_tokens.get(&fixture).unwrap().to_string(),
+ "Bearer abc123".to_string()
+ );
+ let fixture =
+ ModuleSpecifier::resolve_url("http://127.0.0.1:8080/x/mod.ts").unwrap();
+ assert_eq!(auth_tokens.get(&fixture), None);
+ let fixture =
+ ModuleSpecifier::resolve_url("https://deno.land.example.com/x/mod.ts")
+ .unwrap();
+ assert_eq!(auth_tokens.get(&fixture), None);
+ let fixture =
+ ModuleSpecifier::resolve_url("https://deno.land:8080/x/mod.ts").unwrap();
+ assert_eq!(auth_tokens.get(&fixture), None);
+ }
+
+ #[test]
+ fn test_auth_tokens_multiple() {
+ let auth_tokens =
+ AuthTokens::new(Some("abc123@deno.land;def456@example.com".to_string()));
+ let fixture =
+ ModuleSpecifier::resolve_url("https://deno.land/x/mod.ts").unwrap();
+ assert_eq!(
+ auth_tokens.get(&fixture).unwrap().to_string(),
+ "Bearer abc123".to_string()
+ );
+ let fixture =
+ ModuleSpecifier::resolve_url("http://example.com/a/file.ts").unwrap();
+ assert_eq!(
+ auth_tokens.get(&fixture).unwrap().to_string(),
+ "Bearer def456".to_string()
+ );
+ }
+
+ #[test]
+ fn test_auth_tokens_port() {
+ let auth_tokens =
+ AuthTokens::new(Some("abc123@deno.land:8080".to_string()));
+ let fixture =
+ ModuleSpecifier::resolve_url("https://deno.land/x/mod.ts").unwrap();
+ assert_eq!(auth_tokens.get(&fixture), None);
+ let fixture =
+ ModuleSpecifier::resolve_url("http://deno.land:8080/x/mod.ts").unwrap();
+ assert_eq!(
+ auth_tokens.get(&fixture).unwrap().to_string(),
+ "Bearer abc123".to_string()
+ );
+ }
+
+ #[test]
+ fn test_auth_tokens_contain_at() {
+ let auth_tokens = AuthTokens::new(Some("abc@123@deno.land".to_string()));
+ let fixture =
+ ModuleSpecifier::resolve_url("https://deno.land/x/mod.ts").unwrap();
+ assert_eq!(
+ auth_tokens.get(&fixture).unwrap().to_string(),
+ "Bearer abc@123".to_string()
+ );
+ }
+}
diff --git a/cli/file_fetcher.rs b/cli/file_fetcher.rs
index 23ace672c..722813457 100644
--- a/cli/file_fetcher.rs
+++ b/cli/file_fetcher.rs
@@ -1,9 +1,11 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
+use crate::auth_tokens::AuthTokens;
use crate::colors;
use crate::http_cache::HttpCache;
use crate::http_util::create_http_client;
use crate::http_util::fetch_once;
+use crate::http_util::FetchOnceArgs;
use crate::http_util::FetchOnceResult;
use crate::media_type::MediaType;
use crate::text_encoding;
@@ -19,6 +21,7 @@ use deno_core::futures::future::FutureExt;
use deno_core::ModuleSpecifier;
use deno_runtime::deno_fetch::reqwest;
use std::collections::HashMap;
+use std::env;
use std::fs;
use std::future::Future;
use std::io::Read;
@@ -27,6 +30,7 @@ use std::pin::Pin;
use std::sync::Arc;
use std::sync::Mutex;
+static DENO_AUTH_TOKENS: &str = "DENO_AUTH_TOKENS";
pub const SUPPORTED_SCHEMES: [&str; 4] = ["data", "file", "http", "https"];
/// A structure representing a source file.
@@ -308,6 +312,7 @@ fn strip_shebang(mut value: String) -> String {
/// A structure for resolving, fetching and caching source files.
#[derive(Clone)]
pub struct FileFetcher {
+ auth_tokens: AuthTokens,
allow_remote: bool,
cache: FileCache,
cache_setting: CacheSetting,
@@ -323,8 +328,9 @@ impl FileFetcher {
ca_data: Option<Vec<u8>>,
) -> Result<Self, AnyError> {
Ok(Self {
+ auth_tokens: AuthTokens::new(env::var(DENO_AUTH_TOKENS).ok()),
allow_remote,
- cache: FileCache::default(),
+ cache: Default::default(),
cache_setting,
http_cache,
http_client: create_http_client(get_user_agent(), ca_data)?,
@@ -488,17 +494,25 @@ impl FileFetcher {
info!("{} {}", colors::green("Download"), specifier);
- let file_fetcher = self.clone();
- let cached_etag = match self.http_cache.get(specifier.as_url()) {
+ let maybe_etag = match self.http_cache.get(specifier.as_url()) {
Ok((_, headers)) => headers.get("etag").cloned(),
_ => None,
};
+ let maybe_auth_token = self.auth_tokens.get(&specifier);
let specifier = specifier.clone();
let permissions = permissions.clone();
- let http_client = self.http_client.clone();
+ let client = self.http_client.clone();
+ let file_fetcher = self.clone();
// A single pass of fetch either yields code or yields a redirect.
async move {
- match fetch_once(http_client, specifier.as_url(), cached_etag).await? {
+ match fetch_once(FetchOnceArgs {
+ client,
+ url: specifier.as_url().clone(),
+ maybe_etag,
+ maybe_auth_token,
+ })
+ .await?
+ {
FetchOnceResult::NotModified => {
let file = file_fetcher.fetch_cached(&specifier, 10)?.unwrap();
Ok(file)
diff --git a/cli/flags.rs b/cli/flags.rs
index f7c83294c..80ad6e240 100644
--- a/cli/flags.rs
+++ b/cli/flags.rs
@@ -219,18 +219,22 @@ impl From<Flags> for PermissionsOptions {
}
}
-static ENV_VARIABLES_HELP: &str = "ENVIRONMENT VARIABLES:
+static ENV_VARIABLES_HELP: &str = r#"ENVIRONMENT VARIABLES:
+ DENO_AUTH_TOKENS A semi-colon separated list of bearer tokens and
+ hostnames to use when fetching remote modules from
+ private repositories
+ (e.g. "abcde12345@deno.land;54321edcba@github.com")
+ DENO_CERT Load certificate authority from PEM encoded file
DENO_DIR Set the cache directory
DENO_INSTALL_ROOT Set deno install's output directory
(defaults to $HOME/.deno/bin)
- DENO_CERT Load certificate authority from PEM encoded file
- NO_COLOR Set to disable color
HTTP_PROXY Proxy address for HTTP requests
(module downloads, fetch)
HTTPS_PROXY Proxy address for HTTPS requests
(module downloads, fetch)
+ NO_COLOR Set to disable color
NO_PROXY Comma-separated list of hosts which do not use a proxy
- (module downloads, fetch)";
+ (module downloads, fetch)"#;
static DENO_HELP: &str = "A secure JavaScript and TypeScript runtime
diff --git a/cli/http_util.rs b/cli/http_util.rs
index 437b9355c..69778a101 100644
--- a/cli/http_util.rs
+++ b/cli/http_util.rs
@@ -1,11 +1,14 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
+use crate::auth_tokens::AuthToken;
+
use deno_core::error::generic_error;
use deno_core::error::AnyError;
use deno_core::url::Url;
use deno_runtime::deno_fetch::reqwest;
use deno_runtime::deno_fetch::reqwest::header::HeaderMap;
use deno_runtime::deno_fetch::reqwest::header::HeaderValue;
+use deno_runtime::deno_fetch::reqwest::header::AUTHORIZATION;
use deno_runtime::deno_fetch::reqwest::header::IF_NONE_MATCH;
use deno_runtime::deno_fetch::reqwest::header::LOCATION;
use deno_runtime::deno_fetch::reqwest::header::USER_AGENT;
@@ -76,24 +79,33 @@ pub enum FetchOnceResult {
Redirect(Url, HeadersMap),
}
+#[derive(Debug)]
+pub struct FetchOnceArgs {
+ pub client: Client,
+ pub url: Url,
+ pub maybe_etag: Option<String>,
+ pub maybe_auth_token: Option<AuthToken>,
+}
+
/// Asynchronously fetches the given HTTP URL one pass only.
/// If no redirect is present and no error occurs,
/// yields Code(ResultPayload).
/// If redirect occurs, does not follow and
/// yields Redirect(url).
pub async fn fetch_once(
- client: Client,
- url: &Url,
- cached_etag: Option<String>,
+ args: FetchOnceArgs,
) -> Result<FetchOnceResult, AnyError> {
- let url = url.clone();
+ let mut request = args.client.get(args.url.clone());
- let mut request = client.get(url.clone());
-
- if let Some(etag) = cached_etag {
+ if let Some(etag) = args.maybe_etag {
let if_none_match_val = HeaderValue::from_str(&etag).unwrap();
request = request.header(IF_NONE_MATCH, if_none_match_val);
}
+ if let Some(auth_token) = args.maybe_auth_token {
+ let authorization_val =
+ HeaderValue::from_str(&auth_token.to_string()).unwrap();
+ request = request.header(AUTHORIZATION, authorization_val);
+ }
let response = request.send().await?;
if response.status() == StatusCode::NOT_MODIFIED {
@@ -126,20 +138,23 @@ pub async fn fetch_once(
if let Some(location) = response.headers().get(LOCATION) {
let location_string = location.to_str().unwrap();
debug!("Redirecting to {:?}...", &location_string);
- let new_url = resolve_url_from_location(&url, location_string);
+ let new_url = resolve_url_from_location(&args.url, location_string);
return Ok(FetchOnceResult::Redirect(new_url, headers_));
} else {
return Err(generic_error(format!(
"Redirection from '{}' did not provide location header",
- url
+ args.url
)));
}
}
if response.status().is_client_error() || response.status().is_server_error()
{
- let err =
- generic_error(format!("Import '{}' failed: {}", &url, response.status()));
+ let err = generic_error(format!(
+ "Import '{}' failed: {}",
+ args.url,
+ response.status()
+ ));
return Err(err);
}
@@ -165,7 +180,13 @@ mod tests {
let url =
Url::parse("http://127.0.0.1:4545/cli/tests/fixture.json").unwrap();
let client = create_test_client(None);
- let result = fetch_once(client, &url, None).await;
+ let result = fetch_once(FetchOnceArgs {
+ client,
+ url,
+ maybe_etag: None,
+ maybe_auth_token: None,
+ })
+ .await;
if let Ok(FetchOnceResult::Code(body, headers)) = result {
assert!(!body.is_empty());
assert_eq!(headers.get("content-type").unwrap(), "application/json");
@@ -185,7 +206,13 @@ mod tests {
)
.unwrap();
let client = create_test_client(None);
- let result = fetch_once(client, &url, None).await;
+ let result = fetch_once(FetchOnceArgs {
+ client,
+ url,
+ maybe_etag: None,
+ maybe_auth_token: None,
+ })
+ .await;
if let Ok(FetchOnceResult::Code(body, headers)) = result {
assert_eq!(String::from_utf8(body).unwrap(), "console.log('gzip')");
assert_eq!(
@@ -204,7 +231,13 @@ mod tests {
let _http_server_guard = test_util::http_server();
let url = Url::parse("http://127.0.0.1:4545/etag_script.ts").unwrap();
let client = create_test_client(None);
- let result = fetch_once(client.clone(), &url, None).await;
+ let result = fetch_once(FetchOnceArgs {
+ client: client.clone(),
+ url: url.clone(),
+ maybe_etag: None,
+ maybe_auth_token: None,
+ })
+ .await;
if let Ok(FetchOnceResult::Code(body, headers)) = result {
assert!(!body.is_empty());
assert_eq!(String::from_utf8(body).unwrap(), "console.log('etag')");
@@ -217,8 +250,13 @@ mod tests {
panic!();
}
- let res =
- fetch_once(client, &url, Some("33a64df551425fcc55e".to_string())).await;
+ let res = fetch_once(FetchOnceArgs {
+ client,
+ url,
+ maybe_etag: Some("33a64df551425fcc55e".to_string()),
+ maybe_auth_token: None,
+ })
+ .await;
assert_eq!(res.unwrap(), FetchOnceResult::NotModified);
}
@@ -231,7 +269,13 @@ mod tests {
)
.unwrap();
let client = create_test_client(None);
- let result = fetch_once(client, &url, None).await;
+ let result = fetch_once(FetchOnceArgs {
+ client,
+ url,
+ maybe_etag: None,
+ maybe_auth_token: None,
+ })
+ .await;
if let Ok(FetchOnceResult::Code(body, headers)) = result {
assert!(!body.is_empty());
assert_eq!(String::from_utf8(body).unwrap(), "console.log('brotli');");
@@ -256,7 +300,13 @@ mod tests {
let target_url =
Url::parse("http://localhost:4545/cli/tests/fixture.json").unwrap();
let client = create_test_client(None);
- let result = fetch_once(client, &url, None).await;
+ let result = fetch_once(FetchOnceArgs {
+ client,
+ url,
+ maybe_etag: None,
+ maybe_auth_token: None,
+ })
+ .await;
if let Ok(FetchOnceResult::Redirect(url, _)) = result {
assert_eq!(url, target_url);
} else {
@@ -322,7 +372,13 @@ mod tests {
),
)
.unwrap();
- let result = fetch_once(client, &url, None).await;
+ let result = fetch_once(FetchOnceArgs {
+ client,
+ url,
+ maybe_etag: None,
+ maybe_auth_token: None,
+ })
+ .await;
if let Ok(FetchOnceResult::Code(body, headers)) = result {
assert!(!body.is_empty());
assert_eq!(headers.get("content-type").unwrap(), "application/json");
@@ -354,7 +410,13 @@ mod tests {
),
)
.unwrap();
- let result = fetch_once(client, &url, None).await;
+ let result = fetch_once(FetchOnceArgs {
+ client,
+ url,
+ maybe_etag: None,
+ maybe_auth_token: None,
+ })
+ .await;
if let Ok(FetchOnceResult::Code(body, headers)) = result {
assert_eq!(String::from_utf8(body).unwrap(), "console.log('gzip')");
assert_eq!(
@@ -385,7 +447,13 @@ mod tests {
),
)
.unwrap();
- let result = fetch_once(client.clone(), &url, None).await;
+ let result = fetch_once(FetchOnceArgs {
+ client: client.clone(),
+ url: url.clone(),
+ maybe_etag: None,
+ maybe_auth_token: None,
+ })
+ .await;
if let Ok(FetchOnceResult::Code(body, headers)) = result {
assert!(!body.is_empty());
assert_eq!(String::from_utf8(body).unwrap(), "console.log('etag')");
@@ -399,8 +467,13 @@ mod tests {
panic!();
}
- let res =
- fetch_once(client, &url, Some("33a64df551425fcc55e".to_string())).await;
+ let res = fetch_once(FetchOnceArgs {
+ client,
+ url,
+ maybe_etag: Some("33a64df551425fcc55e".to_string()),
+ maybe_auth_token: None,
+ })
+ .await;
assert_eq!(res.unwrap(), FetchOnceResult::NotModified);
}
@@ -425,7 +498,13 @@ mod tests {
),
)
.unwrap();
- let result = fetch_once(client, &url, None).await;
+ let result = fetch_once(FetchOnceArgs {
+ client,
+ url,
+ maybe_etag: None,
+ maybe_auth_token: None,
+ })
+ .await;
if let Ok(FetchOnceResult::Code(body, headers)) = result {
assert!(!body.is_empty());
assert_eq!(String::from_utf8(body).unwrap(), "console.log('brotli');");
@@ -446,7 +525,13 @@ mod tests {
let url_str = "http://127.0.0.1:4545/bad_redirect";
let url = Url::parse(url_str).unwrap();
let client = create_test_client(None);
- let result = fetch_once(client, &url, None).await;
+ let result = fetch_once(FetchOnceArgs {
+ client,
+ url,
+ maybe_etag: None,
+ maybe_auth_token: None,
+ })
+ .await;
assert!(result.is_err());
let err = result.unwrap_err();
// Check that the error message contains the original URL
diff --git a/cli/main.rs b/cli/main.rs
index d98313f54..2addfa0d5 100644
--- a/cli/main.rs
+++ b/cli/main.rs
@@ -8,6 +8,7 @@ extern crate lazy_static;
extern crate log;
mod ast;
+mod auth_tokens;
mod checksum;
mod colors;
mod deno_dir;
diff --git a/cli/tests/integration_tests.rs b/cli/tests/integration_tests.rs
index 15da2359d..a25a51b20 100644
--- a/cli/tests/integration_tests.rs
+++ b/cli/tests/integration_tests.rs
@@ -208,6 +208,42 @@ mod integration {
assert_eq!("noColor false", util::strip_ansi_codes(stdout_str));
}
+ #[test]
+ fn auth_tokens() {
+ let _g = util::http_server();
+ let output = util::deno_cmd()
+ .current_dir(util::root_path())
+ .arg("run")
+ .arg("http://127.0.0.1:4551/cli/tests/001_hello.js")
+ .stdout(std::process::Stdio::piped())
+ .stderr(std::process::Stdio::piped())
+ .spawn()
+ .unwrap()
+ .wait_with_output()
+ .unwrap();
+ assert!(!output.status.success());
+ let stdout_str = std::str::from_utf8(&output.stdout).unwrap().trim();
+ assert!(stdout_str.is_empty());
+ let stderr_str = std::str::from_utf8(&output.stderr).unwrap().trim();
+ eprintln!("{}", stderr_str);
+ assert!(stderr_str.contains("Import 'http://127.0.0.1:4551/cli/tests/001_hello.js' failed: 404 Not Found"));
+
+ let output = util::deno_cmd()
+ .current_dir(util::root_path())
+ .arg("run")
+ .arg("http://127.0.0.1:4551/cli/tests/001_hello.js")
+ .env("DENO_AUTH_TOKENS", "abcdef123456789@127.0.0.1:4551")
+ .stdout(std::process::Stdio::piped())
+ .stderr(std::process::Stdio::piped())
+ .spawn()
+ .unwrap()
+ .wait_with_output()
+ .unwrap();
+ assert!(output.status.success());
+ let stdout_str = std::str::from_utf8(&output.stdout).unwrap().trim();
+ assert_eq!(util::strip_ansi_codes(stdout_str), "Hello World");
+ }
+
#[cfg(unix)]
#[test]
pub fn test_raw_tty() {
diff --git a/docs/images/private-github-new-token.png b/docs/images/private-github-new-token.png
new file mode 100644
index 000000000..8d112cb5e
--- /dev/null
+++ b/docs/images/private-github-new-token.png
Binary files differ
diff --git a/docs/images/private-github-token-display.png b/docs/images/private-github-token-display.png
new file mode 100644
index 000000000..289f80210
--- /dev/null
+++ b/docs/images/private-github-token-display.png
Binary files differ
diff --git a/docs/images/private-pat.png b/docs/images/private-pat.png
new file mode 100644
index 000000000..4fe9666d0
--- /dev/null
+++ b/docs/images/private-pat.png
Binary files differ
diff --git a/docs/linking_to_external_code/private.md b/docs/linking_to_external_code/private.md
new file mode 100644
index 000000000..b48536f2b
--- /dev/null
+++ b/docs/linking_to_external_code/private.md
@@ -0,0 +1,75 @@
+## Private modules and repositories
+
+There maybe instances where you want to load a remote module that is located in
+a _private_ repository, like a private repository on GitHub.
+
+Deno supports sending bearer tokens when requesting a remote module. Bearer
+tokens are the predominate type of access token used with OAuth 2.0 and is
+broadly supported by hosting services (e.g. GitHub, Gitlab, BitBucket,
+Cloudsmith, etc.).
+
+### DENO_AUTH_TOKENS
+
+The Deno CLI will look for an environment variable named `DENO_AUTH_TOKENS` to
+determine what authentication tokens it should consider using when requesting
+remote modules. The value of the environment variable is in the format of a _n_
+number of tokens deliminated by a semi-colon (`;`) where each token is in the
+format of `{token}@{hostname[:port]}`.
+
+For example a single token for would look something like this:
+
+```sh
+DENO_AUTH_TOKENS=a1b2c3d4e5f6@deno.land
+```
+
+And multiple tokens would look like this:
+
+```sh
+DENO_AUTH_TOKENS=a1b2c3d4e5f6@deno.land;f1e2d3c4b5a6@example.com:8080
+```
+
+When Deno goes to fetch a remote module, where the hostname matches the hostname
+of the remote module, Deno will set the `Authorization` header of the request to
+the value of `Bearer {token}`. This allows the remote server to recognize that
+the request is an authorized request tied to a specific authenticated user, and
+provide access to the appropriate resources and modules on the server.
+
+### GitHub
+
+To be able to access private repositories on GitHub, you would need to issue
+yourself a _personal access token_. You do this by logging into GitHub and going
+under _Settings -> Developer settings -> Personal access tokens_:
+
+![Personal access tokens settings on GitHub](../images/private-pat.png)
+
+You would then choose to _Generate new token_ and give your token a description
+and appropriate access:
+
+![Creating a new personal access token on GitHub](../images/private-github-new-token.png)
+
+And once created GitHub will display the new token a single time, the value of
+which you would want to use in the environment variable:
+
+![Display of newly created token on GitHub](../images/private-github-token-display.png)
+
+In order to access modules that are contained in a private repository on GitHub,
+you would want to use the generated token in the `DENO_AUTH_TOKENS` environment
+variable scoped to the `raw.githubusercontent.com` hostname. For example:
+
+```sh
+DENO_AUTH_TOKENS=a1b2c3d4e5f6@raw.githubusercontent.com
+```
+
+This should allow Deno to access any modules that the user who the token was
+issued for has access to.
+
+When the token is incorrect, or the user does not have access to the module,
+GitHub will issue a `404 Not Found` status, instead of an unauthorized status.
+So if you are getting errors that the modules you are trying to access are not
+found on the command line, check the environment variable settings and the
+personal access token settings.
+
+In addition, `deno run -L debug` should print out a debug message about the
+number of tokens that are parsed out of the environment variable. It will print
+an error message if it feels any of the tokens are malformed. It won't print any
+details about the tokens for security purposes.
diff --git a/docs/toc.json b/docs/toc.json
index 0b740c07b..8020b816c 100644
--- a/docs/toc.json
+++ b/docs/toc.json
@@ -31,6 +31,7 @@
"reloading_modules": "Reloading modules",
"integrity_checking": "Integrity checking",
"proxies": "Proxies",
+ "private": "Private modules",
"import_maps": "Import maps"
}
},
diff --git a/test_util/src/lib.rs b/test_util/src/lib.rs
index 37b1819d8..a2c0b2396 100644
--- a/test_util/src/lib.rs
+++ b/test_util/src/lib.rs
@@ -49,11 +49,13 @@ use tokio_rustls::TlsAcceptor;
use tokio_tungstenite::accept_async;
const PORT: u16 = 4545;
+const TEST_AUTH_TOKEN: &str = "abcdef123456789";
const REDIRECT_PORT: u16 = 4546;
const ANOTHER_REDIRECT_PORT: u16 = 4547;
const DOUBLE_REDIRECTS_PORT: u16 = 4548;
const INF_REDIRECTS_PORT: u16 = 4549;
const REDIRECT_ABSOLUTE_PORT: u16 = 4550;
+const AUTH_REDIRECT_PORT: u16 = 4551;
const HTTPS_PORT: u16 = 5545;
const WS_PORT: u16 = 4242;
const WSS_PORT: u16 = 4243;
@@ -201,6 +203,25 @@ async fn another_redirect(req: Request<Body>) -> hyper::Result<Response<Body>> {
Ok(redirect_resp(url))
}
+async fn auth_redirect(req: Request<Body>) -> hyper::Result<Response<Body>> {
+ if let Some(auth) = req
+ .headers()
+ .get("authorization")
+ .map(|v| v.to_str().unwrap())
+ {
+ if auth.to_lowercase() == format!("bearer {}", TEST_AUTH_TOKEN) {
+ let p = req.uri().path();
+ assert_eq!(&p[0..1], "/");
+ let url = format!("http://localhost:{}{}", PORT, p);
+ return Ok(redirect_resp(url));
+ }
+ }
+
+ let mut resp = Response::new(Body::empty());
+ *resp.status_mut() = StatusCode::NOT_FOUND;
+ Ok(resp)
+}
+
async fn run_ws_server(addr: &SocketAddr) {
let listener = TcpListener::bind(addr).await.unwrap();
while let Ok((stream, _addr)) = listener.accept().await {
@@ -666,6 +687,19 @@ async fn wrap_another_redirect_server() {
}
}
+async fn wrap_auth_redirect_server() {
+ let auth_redirect_svc = make_service_fn(|_| async {
+ Ok::<_, Infallible>(service_fn(auth_redirect))
+ });
+ let auth_redirect_addr =
+ SocketAddr::from(([127, 0, 0, 1], AUTH_REDIRECT_PORT));
+ let auth_redirect_server =
+ Server::bind(&auth_redirect_addr).serve(auth_redirect_svc);
+ if let Err(e) = auth_redirect_server.await {
+ eprintln!("Auth redirect error: {:?}", e);
+ }
+}
+
async fn wrap_abs_redirect_server() {
let abs_redirect_svc = make_service_fn(|_| async {
Ok::<_, Infallible>(service_fn(absolute_redirect))
@@ -740,6 +774,7 @@ pub async fn run_all_servers() {
let double_redirects_server_fut = wrap_double_redirect_server();
let inf_redirects_server_fut = wrap_inf_redirect_server();
let another_redirect_server_fut = wrap_another_redirect_server();
+ let auth_redirect_server_fut = wrap_auth_redirect_server();
let abs_redirect_server_fut = wrap_abs_redirect_server();
let ws_addr = SocketAddr::from(([127, 0, 0, 1], WS_PORT));
@@ -756,6 +791,7 @@ pub async fn run_all_servers() {
ws_server_fut,
wss_server_fut,
another_redirect_server_fut,
+ auth_redirect_server_fut,
inf_redirects_server_fut,
double_redirects_server_fut,
abs_redirect_server_fut,