Skip to content

Commit 97aae60

Browse files
authored
Add stronger typing using the NewType pattern (#39)
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.
1 parent 49fef91 commit 97aae60

File tree

6 files changed

+1138
-445
lines changed

6 files changed

+1138
-445
lines changed

.DS_Store

8 KB
Binary file not shown.

Cargo.toml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,15 @@ description = "Bindings for exchanging OAuth 2 tokens"
77
repository = "https://github.com/ramosbugs/oauth2-rs"
88

99
[dependencies]
10+
base64 = "0.9"
1011
curl = "0.4.0"
1112
failure = "0.1"
1213
failure_derive = "0.1"
13-
log = "0.3"
14+
rand = "0.4"
1415
serde = "1.0"
1516
serde_json = "1.0"
1617
serde_derive = "1.0"
1718
url = "1.0"
1819

1920
[dev-dependencies]
20-
base64 = "0.9"
2121
mockito = "0.8.2"
22-
rand = "0.4"

examples/github.rs

Lines changed: 99 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -18,93 +18,136 @@ extern crate oauth2;
1818
extern crate rand;
1919
extern crate url;
2020

21+
use oauth2::prelude::*;
22+
use oauth2::{
23+
AuthorizationCode,
24+
AuthUrl,
25+
ClientId,
26+
ClientSecret,
27+
CsrfToken,
28+
RedirectUrl,
29+
Scope,
30+
Token,
31+
TokenUrl,
32+
};
2133
use oauth2::basic::BasicClient;
22-
use rand::{thread_rng, Rng};
2334
use std::env;
2435
use std::net::TcpListener;
2536
use std::io::{BufRead, BufReader, Write};
2637
use url::Url;
2738

2839
fn main() {
29-
let github_client_id = env::var("GITHUB_CLIENT_ID").expect("Missing the GITHUB_CLIENT_ID environment variable.");
30-
let github_client_secret = env::var("GITHUB_CLIENT_SECRET").expect("Missing the GITHUB_CLIENT_SECRET environment variable.");
31-
let auth_url = "https://github.com/login/oauth/authorize";
32-
let token_url = "https://github.com/login/oauth/access_token";
40+
let github_client_id =
41+
ClientId::new(
42+
env::var("GITHUB_CLIENT_ID")
43+
.expect("Missing the GITHUB_CLIENT_ID environment variable.")
44+
);
45+
let github_client_secret =
46+
ClientSecret::new(
47+
env::var("GITHUB_CLIENT_SECRET")
48+
.expect("Missing the GITHUB_CLIENT_SECRET environment variable.")
49+
);
50+
let auth_url =
51+
AuthUrl::new(
52+
Url::parse("https://github.com/login/oauth/authorize")
53+
.expect("Invalid authorization endpoint URL")
54+
);
55+
let token_url =
56+
TokenUrl::new(
57+
Url::parse("https://github.com/login/oauth/access_token")
58+
.expect("Invalid token endpoint URL")
59+
);
3360

3461
// Set up the config for the Github OAuth2 process.
3562
let client =
3663
BasicClient::new(github_client_id, Some(github_client_secret), auth_url, token_url)
37-
.expect("failed to create client")
38-
3964
// This example is requesting access to the user's public repos and email.
40-
.add_scope("public_repo")
41-
.add_scope("user:email")
65+
.add_scope(Scope::new("public_repo".to_string()))
66+
.add_scope(Scope::new("user:email".to_string()))
4267

4368
// This example will be running its own server at localhost:8080.
4469
// See below for the server implementation.
45-
.set_redirect_url("http://localhost:8080");
46-
47-
let mut rng = thread_rng();
48-
// Generate a 128-bit random string for CSRF protection (each time!).
49-
let random_bytes: Vec<u8> = (0..16).map(|_| rng.gen::<u8>()).collect();
50-
let csrf_state = base64::encode(&random_bytes);
70+
.set_redirect_url(
71+
RedirectUrl::new(
72+
Url::parse("http://localhost:8080")
73+
.expect("Invalid redirect URL")
74+
)
75+
);
5176

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

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

57-
// These variables will store the code & state retrieved during the authorization process.
58-
let mut code = String::new();
59-
let mut state = String::new();
60-
6182
// A very naive implementation of the redirect server.
6283
let listener = TcpListener::bind("127.0.0.1:8080").unwrap();
6384
for stream in listener.incoming() {
64-
match stream {
65-
Ok(mut stream) => {
66-
{
67-
let mut reader = BufReader::new(&stream);
85+
if let Ok(mut stream) = stream {
86+
let code;
87+
let state;
88+
{
89+
let mut reader = BufReader::new(&stream);
6890

69-
let mut request_line = String::new();
70-
reader.read_line(&mut request_line).unwrap();
91+
let mut request_line = String::new();
92+
reader.read_line(&mut request_line).unwrap();
7193

72-
let redirect_url = request_line.split_whitespace().nth(1).unwrap();
73-
let url = Url::parse(&("http://localhost".to_string() + redirect_url)).unwrap();
94+
let redirect_url = request_line.split_whitespace().nth(1).unwrap();
95+
let url = Url::parse(&("http://localhost".to_string() + redirect_url)).unwrap();
7496

75-
let code_pair = url.query_pairs().find(|pair| {
76-
let &(ref key, _) = pair;
77-
key == "code"
78-
}).unwrap();
97+
let code_pair = url.query_pairs().find(|pair| {
98+
let &(ref key, _) = pair;
99+
key == "code"
100+
}).unwrap();
79101

80-
let (_, value) = code_pair;
81-
code = value.into_owned();
102+
let (_, value) = code_pair;
103+
code = AuthorizationCode::new(value.into_owned());
82104

83-
let state_pair = url.query_pairs().find(|pair| {
84-
let &(ref key, _) = pair;
85-
key == "state"
86-
}).unwrap();
105+
let state_pair = url.query_pairs().find(|pair| {
106+
let &(ref key, _) = pair;
107+
key == "state"
108+
}).unwrap();
87109

88-
let (_, value) = state_pair;
89-
state = value.into_owned();
90-
}
91-
92-
let message = "Go back to your terminal :)";
93-
let response = format!("HTTP/1.1 200 OK\r\ncontent-length: {}\r\n\r\n{}", message.len(), message);
94-
stream.write_all(response.as_bytes()).unwrap();
110+
let (_, value) = state_pair;
111+
state = CsrfToken::new(value.into_owned());
112+
}
95113

96-
// The server will terminate itself after collecting the first code.
97-
break;
114+
let message = "Go back to your terminal :)";
115+
let response =
116+
format!("HTTP/1.1 200 OK\r\ncontent-length: {}\r\n\r\n{}", message.len(), message);
117+
stream.write_all(response.as_bytes()).unwrap();
118+
119+
println!("Github returned the following code:\n{}\n", code.secret());
120+
println!(
121+
"Github returned the following state:\n{} (expected `{}`)\n",
122+
state.secret(),
123+
csrf_state.secret()
124+
);
125+
126+
// Exchange the code with a token.
127+
let token_res = client.exchange_code(code);
128+
129+
println!("Github returned the following token:\n{:?}\n", token_res);
130+
131+
if let Ok(token) = token_res {
132+
// NB: Github returns a single comma-separated "scope" parameter instead of multiple
133+
// space-separated scopes. Github-specific clients can parse this scope into
134+
// multiple scopes by splitting at the commas. Note that it's not safe for the
135+
// library to do this by default because RFC 6749 allows scopes to contain commas.
136+
let scopes =
137+
if let Some(scopes_vec) = token.scopes() {
138+
scopes_vec
139+
.iter()
140+
.map(|comma_separated| comma_separated.split(","))
141+
.flat_map(|inner_scopes| inner_scopes)
142+
.collect::<Vec<_>>()
143+
} else {
144+
Vec::new()
145+
};
146+
println!("Github returned the following scopes:\n{:?}\n", scopes);
98147
}
99-
Err(_) => {},
148+
149+
// The server will terminate itself after collecting the first code.
150+
break;
100151
}
101152
};
102-
103-
println!("Github returned the following code:\n{}\n", code);
104-
println!("Github returned the following state:\n{} (expected `{}`)\n", state, csrf_state);
105-
106-
// Exchange the code with a token.
107-
let token = client.exchange_code(code);
108-
109-
println!("Github returned the following token:\n{:?}\n", token);
110153
}

examples/google.rs

Lines changed: 79 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -18,92 +18,117 @@ extern crate oauth2;
1818
extern crate rand;
1919
extern crate url;
2020

21+
use oauth2::prelude::*;
22+
use oauth2::{
23+
AuthorizationCode,
24+
AuthUrl,
25+
ClientId,
26+
ClientSecret,
27+
CsrfToken,
28+
RedirectUrl,
29+
Scope,
30+
TokenUrl,
31+
};
2132
use oauth2::basic::BasicClient;
22-
use rand::{thread_rng, Rng};
2333
use std::env;
2434
use std::net::TcpListener;
2535
use std::io::{BufRead, BufReader, Write};
2636
use url::Url;
2737

2838
fn main() {
29-
let google_client_id = env::var("GOOGLE_CLIENT_ID").expect("Missing the GOOGLE_CLIENT_ID environment variable.");
30-
let google_client_secret = env::var("GOOGLE_CLIENT_SECRET").expect("Missing the GOOGLE_CLIENT_SECRET environment variable.");
31-
let auth_url = "https://accounts.google.com/o/oauth2/v2/auth";
32-
let token_url = "https://www.googleapis.com/oauth2/v3/token";
39+
let google_client_id =
40+
ClientId::new(
41+
env::var("GOOGLE_CLIENT_ID")
42+
.expect("Missing the GOOGLE_CLIENT_ID environment variable.")
43+
);
44+
let google_client_secret =
45+
ClientSecret::new(
46+
env::var("GOOGLE_CLIENT_SECRET")
47+
.expect("Missing the GOOGLE_CLIENT_SECRET environment variable.")
48+
);
49+
let auth_url =
50+
AuthUrl::new(
51+
Url::parse("https://accounts.google.com/o/oauth2/v2/auth")
52+
.expect("Invalid authorization endpoint URL")
53+
);
54+
let token_url =
55+
TokenUrl::new(
56+
Url::parse("https://www.googleapis.com/oauth2/v3/token")
57+
.expect("Invalid token endpoint URL")
58+
);
3359

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

4267
// This example will be running its own server at localhost:8080.
4368
// See below for the server implementation.
44-
.set_redirect_url("http://localhost:8080");
45-
46-
let mut rng = thread_rng();
47-
// Generate a 128-bit random string for CSRF protection (each time!).
48-
let random_bytes: Vec<u8> = (0..16).map(|_| rng.gen::<u8>()).collect();
49-
let csrf_state = base64::encode(&random_bytes);
69+
.set_redirect_url(
70+
RedirectUrl::new(
71+
Url::parse("http://localhost:8080")
72+
.expect("Invalid redirect URL")
73+
)
74+
);
5075

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

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

56-
// These variables will store the code & state retrieved during the authorization process.
57-
let mut code = String::new();
58-
let mut state = String::new();
59-
6081
// A very naive implementation of the redirect server.
6182
let listener = TcpListener::bind("127.0.0.1:8080").unwrap();
6283
for stream in listener.incoming() {
63-
match stream {
64-
Ok(mut stream) => {
65-
{
66-
let mut reader = BufReader::new(&stream);
84+
if let Ok(mut stream) = stream {
85+
let code;
86+
let state;
87+
{
88+
let mut reader = BufReader::new(&stream);
6789

68-
let mut request_line = String::new();
69-
reader.read_line(&mut request_line).unwrap();
90+
let mut request_line = String::new();
91+
reader.read_line(&mut request_line).unwrap();
7092

71-
let redirect_url = request_line.split_whitespace().nth(1).unwrap();
72-
let url = Url::parse(&("http://localhost".to_string() + redirect_url)).unwrap();
93+
let redirect_url = request_line.split_whitespace().nth(1).unwrap();
94+
let url = Url::parse(&("http://localhost".to_string() + redirect_url)).unwrap();
7395

74-
let code_pair = url.query_pairs().find(|pair| {
75-
let &(ref key, _) = pair;
76-
key == "code"
77-
}).unwrap();
96+
let code_pair = url.query_pairs().find(|pair| {
97+
let &(ref key, _) = pair;
98+
key == "code"
99+
}).unwrap();
78100

79-
let (_, value) = code_pair;
80-
code = value.into_owned();
101+
let (_, value) = code_pair;
102+
code = AuthorizationCode::new(value.into_owned());
81103

82-
let state_pair = url.query_pairs().find(|pair| {
83-
let &(ref key, _) = pair;
84-
key == "state"
85-
}).unwrap();
104+
let state_pair = url.query_pairs().find(|pair| {
105+
let &(ref key, _) = pair;
106+
key == "state"
107+
}).unwrap();
86108

87-
let (_, value) = state_pair;
88-
state = value.into_owned();
89-
}
109+
let (_, value) = state_pair;
110+
state = CsrfToken::new(value.into_owned());
111+
}
90112

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

95-
// The server will terminate itself after collecting the first code.
96-
break;
97-
}
98-
Err(_) => {},
99-
}
100-
};
118+
println!("Google returned the following code:\n{}\n", code.secret());
119+
println!(
120+
"Google returned the following state:\n{} (expected `{}`)\n",
121+
state.secret(),
122+
csrf_state.secret()
123+
);
101124

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

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

108-
println!("Google returned the following token:\n{:?}\n", token);
130+
// The server will terminate itself after collecting the first code.
131+
break;
132+
}
133+
};
109134
}

0 commit comments

Comments
 (0)