Skip to content

Commit a2298cf

Browse files
authored
Merge pull request #2 from rhaskia/main
Ability to request with a certain scope
2 parents 4a3732e + 6ca0cb1 commit a2298cf

File tree

3 files changed

+122
-51
lines changed

3 files changed

+122
-51
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,6 @@ OPTIONS:
4040
-h, --host <HOST> The host to authenticate with
4141
--help Print help information
4242
-r, --refresh <REFRESH> A Refresh Token to exchange
43+
-s, --scope <SCOPE> The scope required for the auth app
4344
-V, --version Print version information
4445
```

src/lib.rs

Lines changed: 115 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
1-
2-
use std::{result::Result, thread, time, fmt};
31
use std::collections::HashMap;
2+
use std::{fmt, result::Result, thread, time};
43

5-
use chrono::{DateTime, Duration};
64
use chrono::offset::Utc;
5+
use chrono::{DateTime, Duration};
76

87
mod util;
98

10-
#[derive(Debug, Default, Clone, serde_derive::Serialize)]
9+
#[derive(Debug, Default, Clone, serde_derive::Serialize, serde_derive::Deserialize)]
1110
pub struct Credential {
1211
pub token: String,
1312
pub expiry: String,
@@ -26,25 +25,24 @@ impl Credential {
2625
pub fn is_expired(&self) -> bool {
2726
let exp = match DateTime::parse_from_rfc3339(self.expiry.as_str()) {
2827
Ok(time) => time,
29-
Err(_) => return false
28+
Err(_) => return false,
3029
};
3130
let now = Utc::now();
3231
now > exp
3332
}
3433
}
3534

36-
3735
#[derive(Debug, Clone)]
3836
pub enum DeviceFlowError {
39-
HttpError(String),
40-
GitHubError(String),
37+
HttpError(String),
38+
GitHubError(String),
4139
}
4240

4341
impl fmt::Display for DeviceFlowError {
4442
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
4543
match self {
4644
DeviceFlowError::HttpError(string) => write!(f, "DeviceFlowError: {}", string),
47-
DeviceFlowError::GitHubError(string) => write!(f, "DeviceFlowError: {}", string)
45+
DeviceFlowError::GitHubError(string) => write!(f, "DeviceFlowError: {}", string),
4846
}
4947
}
5048
}
@@ -57,52 +55,83 @@ impl From<reqwest::Error> for DeviceFlowError {
5755
}
5856
}
5957

60-
pub fn authorize(client_id: String, host: Option<String>) -> Result<Credential, DeviceFlowError> {
58+
pub fn authorize(
59+
client_id: String,
60+
host: Option<String>,
61+
scope: Option<String>,
62+
) -> Result<Credential, DeviceFlowError> {
6163
let my_string: String;
6264
let thost = match host {
6365
Some(string) => {
6466
my_string = string;
6567
Some(my_string.as_str())
66-
},
67-
None => None
68+
}
69+
None => None,
6870
};
6971

70-
let mut flow = DeviceFlow::start(client_id.as_str(), thost)?;
72+
let binding: String;
73+
let tscope = match scope {
74+
Some(string) => {
75+
binding = string;
76+
Some(binding.as_str())
77+
}
78+
None => None,
79+
};
80+
81+
let mut flow = DeviceFlow::start(client_id.as_str(), thost, tscope)?;
7182

7283
// eprintln!("res is {:?}", res);
73-
eprintln!("Please visit {} in your browser", flow.verification_uri.clone().unwrap());
84+
eprintln!(
85+
"Please visit {} in your browser",
86+
flow.verification_uri.clone().unwrap()
87+
);
7488
eprintln!("And enter code: {}", flow.user_code.clone().unwrap());
7589

7690
thread::sleep(FIVE_SECONDS);
7791

7892
flow.poll(20)
7993
}
8094

81-
pub fn refresh(client_id: &str, refresh_token: &str, host: Option<String>) -> Result<Credential, DeviceFlowError> {
95+
pub fn refresh(
96+
client_id: &str,
97+
refresh_token: &str,
98+
host: Option<String>,
99+
scope: Option<String>,
100+
) -> Result<Credential, DeviceFlowError> {
82101
let my_string: String;
83102
let thost = match host {
84103
Some(string) => {
85104
my_string = string;
86105
Some(my_string.as_str())
87-
},
88-
None => None
106+
}
107+
None => None,
89108
};
90109

91-
refresh_access_token(client_id, refresh_token, thost)
110+
let scope_binding;
111+
let tscope = match scope {
112+
Some(string) => {
113+
scope_binding = string;
114+
Some(scope_binding.as_str())
115+
}
116+
None => None,
117+
};
118+
119+
refresh_access_token(client_id, refresh_token, thost, tscope)
92120
}
93121

94122
#[derive(Debug, Clone)]
95123
pub enum DeviceFlowState {
96124
Pending,
97125
Processing(time::Duration),
98126
Success(Credential),
99-
Failure(DeviceFlowError)
127+
Failure(DeviceFlowError),
100128
}
101129

102130
#[derive(Clone)]
103131
pub struct DeviceFlow {
104132
pub host: String,
105133
pub client_id: String,
134+
pub scope: String,
106135
pub user_code: Option<String>,
107136
pub device_code: Option<String>,
108137
pub verification_uri: Option<String>,
@@ -112,12 +141,16 @@ pub struct DeviceFlow {
112141
const FIVE_SECONDS: time::Duration = time::Duration::new(5, 0);
113142

114143
impl DeviceFlow {
115-
pub fn new(client_id: &str, maybe_host: Option<&str>) -> Self {
116-
Self{
144+
pub fn new(client_id: &str, maybe_host: Option<&str>, scope: Option<&str>) -> Self {
145+
Self {
117146
client_id: String::from(client_id),
147+
scope: match scope {
148+
Some(string) => String::from(string),
149+
None => String::new(),
150+
},
118151
host: match maybe_host {
119152
Some(string) => String::from(string),
120-
None => String::from("github.com")
153+
None => String::from("github.com"),
121154
},
122155
user_code: None,
123156
device_code: None,
@@ -126,31 +159,43 @@ impl DeviceFlow {
126159
}
127160
}
128161

129-
pub fn start(client_id: &str, maybe_host: Option<&str>) -> Result<DeviceFlow, DeviceFlowError> {
130-
let mut flow = DeviceFlow::new(client_id, maybe_host);
162+
pub fn start(
163+
client_id: &str,
164+
maybe_host: Option<&str>,
165+
scope: Option<&str>,
166+
) -> Result<DeviceFlow, DeviceFlowError> {
167+
let mut flow = DeviceFlow::new(client_id, maybe_host, scope);
131168

132169
flow.setup();
133170

134171
match flow.state {
135172
DeviceFlowState::Processing(_) => Ok(flow.to_owned()),
136173
DeviceFlowState::Failure(err) => Err(err),
137-
_ => Err(util::credential_error("Something truly unexpected happened".into()))
174+
_ => Err(util::credential_error(
175+
"Something truly unexpected happened".into(),
176+
)),
138177
}
139178
}
140179

141180
pub fn setup(&mut self) {
142-
let body = format!("client_id={}", &self.client_id);
181+
let body = format!("client_id={}&scope={}", &self.client_id, &self.scope);
143182
let entry_url = format!("https://{}/login/device/code", &self.host);
144183

145184
if let Some(res) = util::send_request(self, entry_url, body) {
146-
if res.contains_key("error") && res.contains_key("error_description"){
147-
self.state = DeviceFlowState::Failure(util::credential_error(res["error_description"].as_str().unwrap().into()))
185+
if res.contains_key("error") && res.contains_key("error_description") {
186+
self.state = DeviceFlowState::Failure(util::credential_error(
187+
res["error_description"].as_str().unwrap().into(),
188+
))
148189
} else if res.contains_key("error") {
149-
self.state = DeviceFlowState::Failure(util::credential_error(format!("Error response: {:?}", res["error"].as_str().unwrap())))
190+
self.state = DeviceFlowState::Failure(util::credential_error(format!(
191+
"Error response: {:?}",
192+
res["error"].as_str().unwrap()
193+
)))
150194
} else {
151195
self.user_code = Some(String::from(res["user_code"].as_str().unwrap()));
152196
self.device_code = Some(String::from(res["device_code"].as_str().unwrap()));
153-
self.verification_uri = Some(String::from(res["verification_uri"].as_str().unwrap()));
197+
self.verification_uri =
198+
Some(String::from(res["verification_uri"].as_str().unwrap()));
154199
self.state = DeviceFlowState::Processing(FIVE_SECONDS);
155200
}
156201
};
@@ -162,51 +207,57 @@ impl DeviceFlow {
162207

163208
if let DeviceFlowState::Processing(interval) = self.state {
164209
if count == iterations {
165-
return Err(util::credential_error("Max poll iterations reached".into()))
210+
return Err(util::credential_error("Max poll iterations reached".into()));
166211
}
167212

168213
thread::sleep(interval);
169214
} else {
170-
break
215+
break;
171216
}
172-
};
217+
}
173218

174219
match &self.state {
175220
DeviceFlowState::Success(cred) => Ok(cred.to_owned()),
176221
DeviceFlowState::Failure(err) => Err(err.to_owned()),
177-
_ => Err(util::credential_error("Unable to fetch credential, sorry :/".into()))
222+
_ => Err(util::credential_error(
223+
"Unable to fetch credential, sorry :/".into(),
224+
)),
178225
}
179226
}
180227

181228
pub fn update(&mut self) {
182229
let poll_url = format!("https://{}/login/oauth/access_token", self.host);
183-
let poll_payload = format!("client_id={}&device_code={}&grant_type=urn:ietf:params:oauth:grant-type:device_code",
230+
let poll_payload = format!(
231+
"client_id={}&device_code={}&grant_type=urn:ietf:params:oauth:grant-type:device_code",
184232
self.client_id,
185233
&self.device_code.clone().unwrap()
186234
);
187235

188236
if let Some(res) = util::send_request(self, poll_url, poll_payload) {
189237
if res.contains_key("error") {
190238
match res["error"].as_str().unwrap() {
191-
"authorization_pending" => {},
239+
"authorization_pending" => {}
192240
"slow_down" => {
193241
if let DeviceFlowState::Processing(current_interval) = self.state {
194-
self.state = DeviceFlowState::Processing(current_interval + FIVE_SECONDS);
242+
self.state =
243+
DeviceFlowState::Processing(current_interval + FIVE_SECONDS);
195244
};
196-
},
245+
}
197246
other_reason => {
198-
self.state = DeviceFlowState::Failure(
199-
util::credential_error(format!("Error checking for token: {}", other_reason))
200-
);
201-
},
247+
self.state = DeviceFlowState::Failure(util::credential_error(format!(
248+
"Error checking for token: {}",
249+
other_reason
250+
)));
251+
}
202252
}
203253
} else {
204254
let mut this_credential = Credential::empty();
205255
this_credential.token = res["access_token"].as_str().unwrap().to_string();
206256

207257
if let Some(expires_in) = res.get("expires_in") {
208258
this_credential.expiry = calculate_expiry(expires_in.as_i64().unwrap());
209-
this_credential.refresh_token = res["refresh_token"].as_str().unwrap().to_string();
259+
this_credential.refresh_token =
260+
res["refresh_token"].as_str().unwrap().to_string();
210261
}
211262

212263
self.state = DeviceFlowState::Success(this_credential);
@@ -222,25 +273,40 @@ fn calculate_expiry(expires_in: i64) -> String {
222273
expiry.to_rfc3339()
223274
}
224275

225-
fn refresh_access_token(client_id: &str, refresh_token: &str, maybe_host: Option<&str>) -> Result<Credential, DeviceFlowError> {
276+
fn refresh_access_token(
277+
client_id: &str,
278+
refresh_token: &str,
279+
maybe_host: Option<&str>,
280+
maybe_scope: Option<&str>,
281+
) -> Result<Credential, DeviceFlowError> {
226282
let host = match maybe_host {
227283
Some(string) => string,
228-
None => "github.com"
284+
None => "github.com",
285+
};
286+
287+
let scope = match maybe_scope {
288+
Some(string) => string,
289+
None => "",
229290
};
230291

231292
let client = reqwest::blocking::Client::new();
232293
let entry_url = format!("https://{}/login/oauth/access_token", &host);
233-
let request_body = format!("client_id={}&refresh_token={}&client_secret=&grant_type=refresh_token",
234-
&client_id, &refresh_token);
294+
let request_body = format!(
295+
"client_id={}&refresh_token={}&client_secret=&grant_type=refresh_token&scope={}",
296+
&client_id, &refresh_token, &scope
297+
);
235298

236-
let res = client.post(&entry_url)
299+
let res = client
300+
.post(&entry_url)
237301
.header("Accept", "application/json")
238302
.body(request_body)
239303
.send()?
240304
.json::<HashMap<String, serde_json::Value>>()?;
241305

242306
if res.contains_key("error") {
243-
Err(util::credential_error(res["error"].as_str().unwrap().into()))
307+
Err(util::credential_error(
308+
res["error"].as_str().unwrap().into(),
309+
))
244310
} else {
245311
let mut credential = Credential::empty();
246312
// eprintln!("res: {:?}", &res);

src/main.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ struct Args {
1818
#[clap(short, long, value_parser)]
1919
host: Option<String>,
2020

21+
/// The scope the user wants to have
22+
#[clap(short, long, value_parser)]
23+
scope: Option<String>,
24+
2125
/// A Refresh Token to exchange
2226
#[clap(short, long, value_parser)]
2327
refresh: Option<String>,
@@ -29,10 +33,10 @@ fn main() {
2933

3034
match args.refresh {
3135
None => {
32-
cred = authorize(args.client_id, args.host);
36+
cred = authorize(args.client_id, args.host, args.scope);
3337
},
3438
Some(rt) => {
35-
cred = refresh(args.client_id.as_str(), rt.as_str(), args.host);
39+
cred = refresh(args.client_id.as_str(), rt.as_str(), args.host, args.scope);
3640
}
3741
}
3842
match cred {

0 commit comments

Comments
 (0)