Skip to content

Commit

Permalink
Add stronger typing using the NewType pattern (#39)
Browse files Browse the repository at this point in the history
NOTE: This is another breaking change that will be part of the 2.0.0 release.

This diff replaces weakly typed `String`s, `Url`s, and other types with new
types generated using the `NewType` pattern. Using stronger types here
should avoid common mistakes (e.g., switching the order of the authorization
and endpoint URLs when instantiating a new `Client`).

In addition to adding a `NewType` trait, this diff adds a `NewSecretType`
trait, which implements `Debug` in a way that redacts the secret. This
behavior avoids a common source of security bugs: logging secrets,
especially when errors occur. Unlike the `NewType` trait, the
`NewSecretType` does not implement `Deref`. Instead, the secret must
be explicitly extracted by calling the `secret` method.

Finally, this PR resolves #28 by having the `authorize_url` method accept a
closure for generating a fresh CSRF token on each invocation. The token is
returned by the method as `#[must_use]`, which the caller should compare
against the response sent by the authorization server to the redirect URI.
Note that `#[must_use]` currently has no effect in this context, but it should
once rust-lang/rust#39524 is resolved.
  • Loading branch information
ramosbugs committed Apr 21, 2018
1 parent 49fef91 commit 97aae60
Show file tree
Hide file tree
Showing 6 changed files with 1,138 additions and 445 deletions.
Binary file added .DS_Store
Binary file not shown.
5 changes: 2 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,15 @@ description = "Bindings for exchanging OAuth 2 tokens"
repository = "https://github.com/ramosbugs/oauth2-rs"

[dependencies]
base64 = "0.9"
curl = "0.4.0"
failure = "0.1"
failure_derive = "0.1"
log = "0.3"
rand = "0.4"
serde = "1.0"
serde_json = "1.0"
serde_derive = "1.0"
url = "1.0"

[dev-dependencies]
base64 = "0.9"
mockito = "0.8.2"
rand = "0.4"
155 changes: 99 additions & 56 deletions examples/github.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,93 +18,136 @@ extern crate oauth2;
extern crate rand;
extern crate url;

use oauth2::prelude::*;
use oauth2::{
AuthorizationCode,
AuthUrl,
ClientId,
ClientSecret,
CsrfToken,
RedirectUrl,
Scope,
Token,
TokenUrl,
};
use oauth2::basic::BasicClient;
use rand::{thread_rng, Rng};
use std::env;
use std::net::TcpListener;
use std::io::{BufRead, BufReader, Write};
use url::Url;

fn main() {
let github_client_id = env::var("GITHUB_CLIENT_ID").expect("Missing the GITHUB_CLIENT_ID environment variable.");
let github_client_secret = env::var("GITHUB_CLIENT_SECRET").expect("Missing the GITHUB_CLIENT_SECRET environment variable.");
let auth_url = "https://github.com/login/oauth/authorize";
let token_url = "https://github.com/login/oauth/access_token";
let github_client_id =
ClientId::new(
env::var("GITHUB_CLIENT_ID")
.expect("Missing the GITHUB_CLIENT_ID environment variable.")
);
let github_client_secret =
ClientSecret::new(
env::var("GITHUB_CLIENT_SECRET")
.expect("Missing the GITHUB_CLIENT_SECRET environment variable.")
);
let auth_url =
AuthUrl::new(
Url::parse("https://github.com/login/oauth/authorize")
.expect("Invalid authorization endpoint URL")
);
let token_url =
TokenUrl::new(
Url::parse("https://github.com/login/oauth/access_token")
.expect("Invalid token endpoint URL")
);

// Set up the config for the Github OAuth2 process.
let client =
BasicClient::new(github_client_id, Some(github_client_secret), auth_url, token_url)
.expect("failed to create client")

// This example is requesting access to the user's public repos and email.
.add_scope("public_repo")
.add_scope("user:email")
.add_scope(Scope::new("public_repo".to_string()))
.add_scope(Scope::new("user:email".to_string()))

// This example will be running its own server at localhost:8080.
// See below for the server implementation.
.set_redirect_url("http://localhost:8080");

let mut rng = thread_rng();
// Generate a 128-bit random string for CSRF protection (each time!).
let random_bytes: Vec<u8> = (0..16).map(|_| rng.gen::<u8>()).collect();
let csrf_state = base64::encode(&random_bytes);
.set_redirect_url(
RedirectUrl::new(
Url::parse("http://localhost:8080")
.expect("Invalid redirect URL")
)
);

// Generate the authorization URL to which we'll redirect the user.
let authorize_url = client.authorize_url(csrf_state.clone());
let (authorize_url, csrf_state) = client.authorize_url(CsrfToken::new_random);

println!("Open this URL in your browser:\n{}\n", authorize_url.to_string());

// These variables will store the code & state retrieved during the authorization process.
let mut code = String::new();
let mut state = String::new();

// A very naive implementation of the redirect server.
let listener = TcpListener::bind("127.0.0.1:8080").unwrap();
for stream in listener.incoming() {
match stream {
Ok(mut stream) => {
{
let mut reader = BufReader::new(&stream);
if let Ok(mut stream) = stream {
let code;
let state;
{
let mut reader = BufReader::new(&stream);

let mut request_line = String::new();
reader.read_line(&mut request_line).unwrap();
let mut request_line = String::new();
reader.read_line(&mut request_line).unwrap();

let redirect_url = request_line.split_whitespace().nth(1).unwrap();
let url = Url::parse(&("http://localhost".to_string() + redirect_url)).unwrap();
let redirect_url = request_line.split_whitespace().nth(1).unwrap();
let url = Url::parse(&("http://localhost".to_string() + redirect_url)).unwrap();

let code_pair = url.query_pairs().find(|pair| {
let &(ref key, _) = pair;
key == "code"
}).unwrap();
let code_pair = url.query_pairs().find(|pair| {
let &(ref key, _) = pair;
key == "code"
}).unwrap();

let (_, value) = code_pair;
code = value.into_owned();
let (_, value) = code_pair;
code = AuthorizationCode::new(value.into_owned());

let state_pair = url.query_pairs().find(|pair| {
let &(ref key, _) = pair;
key == "state"
}).unwrap();
let state_pair = url.query_pairs().find(|pair| {
let &(ref key, _) = pair;
key == "state"
}).unwrap();

let (_, value) = state_pair;
state = value.into_owned();
}

let message = "Go back to your terminal :)";
let response = format!("HTTP/1.1 200 OK\r\ncontent-length: {}\r\n\r\n{}", message.len(), message);
stream.write_all(response.as_bytes()).unwrap();
let (_, value) = state_pair;
state = CsrfToken::new(value.into_owned());
}

// The server will terminate itself after collecting the first code.
break;
let message = "Go back to your terminal :)";
let response =
format!("HTTP/1.1 200 OK\r\ncontent-length: {}\r\n\r\n{}", message.len(), message);
stream.write_all(response.as_bytes()).unwrap();

println!("Github returned the following code:\n{}\n", code.secret());
println!(
"Github returned the following state:\n{} (expected `{}`)\n",
state.secret(),
csrf_state.secret()
);

// Exchange the code with a token.
let token_res = client.exchange_code(code);

println!("Github returned the following token:\n{:?}\n", token_res);

if let Ok(token) = token_res {
// NB: Github returns a single comma-separated "scope" parameter instead of multiple
// space-separated scopes. Github-specific clients can parse this scope into
// multiple scopes by splitting at the commas. Note that it's not safe for the
// library to do this by default because RFC 6749 allows scopes to contain commas.
let scopes =
if let Some(scopes_vec) = token.scopes() {
scopes_vec
.iter()
.map(|comma_separated| comma_separated.split(","))
.flat_map(|inner_scopes| inner_scopes)
.collect::<Vec<_>>()
} else {
Vec::new()
};
println!("Github returned the following scopes:\n{:?}\n", scopes);
}
Err(_) => {},

// The server will terminate itself after collecting the first code.
break;
}
};

println!("Github returned the following code:\n{}\n", code);
println!("Github returned the following state:\n{} (expected `{}`)\n", state, csrf_state);

// Exchange the code with a token.
let token = client.exchange_code(code);

println!("Github returned the following token:\n{:?}\n", token);
}
133 changes: 79 additions & 54 deletions examples/google.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,92 +18,117 @@ extern crate oauth2;
extern crate rand;
extern crate url;

use oauth2::prelude::*;
use oauth2::{
AuthorizationCode,
AuthUrl,
ClientId,
ClientSecret,
CsrfToken,
RedirectUrl,
Scope,
TokenUrl,
};
use oauth2::basic::BasicClient;
use rand::{thread_rng, Rng};
use std::env;
use std::net::TcpListener;
use std::io::{BufRead, BufReader, Write};
use url::Url;

fn main() {
let google_client_id = env::var("GOOGLE_CLIENT_ID").expect("Missing the GOOGLE_CLIENT_ID environment variable.");
let google_client_secret = env::var("GOOGLE_CLIENT_SECRET").expect("Missing the GOOGLE_CLIENT_SECRET environment variable.");
let auth_url = "https://accounts.google.com/o/oauth2/v2/auth";
let token_url = "https://www.googleapis.com/oauth2/v3/token";
let google_client_id =
ClientId::new(
env::var("GOOGLE_CLIENT_ID")
.expect("Missing the GOOGLE_CLIENT_ID environment variable.")
);
let google_client_secret =
ClientSecret::new(
env::var("GOOGLE_CLIENT_SECRET")
.expect("Missing the GOOGLE_CLIENT_SECRET environment variable.")
);
let auth_url =
AuthUrl::new(
Url::parse("https://accounts.google.com/o/oauth2/v2/auth")
.expect("Invalid authorization endpoint URL")
);
let token_url =
TokenUrl::new(
Url::parse("https://www.googleapis.com/oauth2/v3/token")
.expect("Invalid token endpoint URL")
);

// Set up the config for the Google OAuth2 process.
let client =
BasicClient::new(google_client_id, Some(google_client_secret), auth_url, token_url)
.expect("failed to create client")
// This example is requesting access to the "calendar" features and the user's profile.
.add_scope("https://www.googleapis.com/auth/calendar")
.add_scope("https://www.googleapis.com/auth/plus.me")
.add_scope(Scope::new("https://www.googleapis.com/auth/calendar".to_string()))
.add_scope(Scope::new("https://www.googleapis.com/auth/plus.me".to_string()))

// This example will be running its own server at localhost:8080.
// See below for the server implementation.
.set_redirect_url("http://localhost:8080");

let mut rng = thread_rng();
// Generate a 128-bit random string for CSRF protection (each time!).
let random_bytes: Vec<u8> = (0..16).map(|_| rng.gen::<u8>()).collect();
let csrf_state = base64::encode(&random_bytes);
.set_redirect_url(
RedirectUrl::new(
Url::parse("http://localhost:8080")
.expect("Invalid redirect URL")
)
);

// Generate the authorization URL to which we'll redirect the user.
let authorize_url = client.authorize_url(csrf_state.clone());
let (authorize_url, csrf_state) = client.authorize_url(CsrfToken::new_random);

println!("Open this URL in your browser:\n{}\n", authorize_url.to_string());

// These variables will store the code & state retrieved during the authorization process.
let mut code = String::new();
let mut state = String::new();

// A very naive implementation of the redirect server.
let listener = TcpListener::bind("127.0.0.1:8080").unwrap();
for stream in listener.incoming() {
match stream {
Ok(mut stream) => {
{
let mut reader = BufReader::new(&stream);
if let Ok(mut stream) = stream {
let code;
let state;
{
let mut reader = BufReader::new(&stream);

let mut request_line = String::new();
reader.read_line(&mut request_line).unwrap();
let mut request_line = String::new();
reader.read_line(&mut request_line).unwrap();

let redirect_url = request_line.split_whitespace().nth(1).unwrap();
let url = Url::parse(&("http://localhost".to_string() + redirect_url)).unwrap();
let redirect_url = request_line.split_whitespace().nth(1).unwrap();
let url = Url::parse(&("http://localhost".to_string() + redirect_url)).unwrap();

let code_pair = url.query_pairs().find(|pair| {
let &(ref key, _) = pair;
key == "code"
}).unwrap();
let code_pair = url.query_pairs().find(|pair| {
let &(ref key, _) = pair;
key == "code"
}).unwrap();

let (_, value) = code_pair;
code = value.into_owned();
let (_, value) = code_pair;
code = AuthorizationCode::new(value.into_owned());

let state_pair = url.query_pairs().find(|pair| {
let &(ref key, _) = pair;
key == "state"
}).unwrap();
let state_pair = url.query_pairs().find(|pair| {
let &(ref key, _) = pair;
key == "state"
}).unwrap();

let (_, value) = state_pair;
state = value.into_owned();
}
let (_, value) = state_pair;
state = CsrfToken::new(value.into_owned());
}

let message = "Go back to your terminal :)";
let response = format!("HTTP/1.1 200 OK\r\ncontent-length: {}\r\n\r\n{}", message.len(), message);
stream.write_all(response.as_bytes()).unwrap();
let message = "Go back to your terminal :)";
let response =
format!("HTTP/1.1 200 OK\r\ncontent-length: {}\r\n\r\n{}", message.len(), message);
stream.write_all(response.as_bytes()).unwrap();

// The server will terminate itself after collecting the first code.
break;
}
Err(_) => {},
}
};
println!("Google returned the following code:\n{}\n", code.secret());
println!(
"Google returned the following state:\n{} (expected `{}`)\n",
state.secret(),
csrf_state.secret()
);

println!("Google returned the following code:\n{}\n", code);
println!("Google returned the following state:\n{} (expected `{}`)\n", state, csrf_state);
// Exchange the code with a token.
let token = client.exchange_code(code);

// Exchange the code with a token.
let token = client.exchange_code(code);
println!("Google returned the following token:\n{:?}\n", token);

println!("Google returned the following token:\n{:?}\n", token);
// The server will terminate itself after collecting the first code.
break;
}
};
}
Loading

0 comments on commit 97aae60

Please sign in to comment.