Skip to content

Commit

Permalink
Adds the device code authorization flow. (#429)
Browse files Browse the repository at this point in the history
* Add device code flow and examples

* Update README
  • Loading branch information
sreeise authored Apr 14, 2023
1 parent 690ec58 commit a6e8c9b
Show file tree
Hide file tree
Showing 8 changed files with 497 additions and 11 deletions.
8 changes: 4 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "graph-rs-sdk"
version = "1.1.0"
version = "1.1.1"
authors = ["sreeise"]
edition = "2021"
readme = "README.md"
Expand Down Expand Up @@ -36,15 +36,15 @@ serde_json = "1"
url = "2"
lazy_static = "1.4.0"

graph-oauth = { path = "./graph-oauth", version = "1.0.0" }
graph-oauth = { path = "./graph-oauth", version = "1.0.2", default-features=false }
graph-http = { path = "./graph-http", version = "1.1.0", default-features=false }
graph-error = { path = "./graph-error", version = "0.2.2" }
graph-core = { path = "./graph-core", version = "0.4.0" }

[features]
default = ["native-tls"]
native-tls = ["reqwest/native-tls", "graph-http/native-tls"]
rustls-tls = ["reqwest/rustls-tls", "graph-http/rustls-tls"]
native-tls = ["reqwest/native-tls", "graph-http/native-tls", "graph-oauth/native-tls"]
rustls-tls = ["reqwest/rustls-tls", "graph-http/rustls-tls", "graph-oauth/rustls-tls"]
brotli = ["reqwest/brotli"]
deflate = ["reqwest/deflate"]
trust-dns = ["reqwest/trust-dns"]
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
### Available on [crates.io](https://crates.io/crates/graph-rs-sdk)

```toml
graph-rs-sdk = "1.1.0"
graph-rs-sdk = "1.1.1"
tokio = { version = "1.25.0", features = ["full"] }
```

Expand Down Expand Up @@ -229,7 +229,7 @@ The crate can do both an async and blocking requests.

#### Async Client (default)

graph-rs-sdk = "1.1.0"
graph-rs-sdk = "1.1.1"
tokio = { version = "1.25.0", features = ["full"] }

#### Example
Expand Down Expand Up @@ -261,7 +261,7 @@ async fn main() -> GraphResult<()> {
To use the blocking client use the `into_blocking()` method. You should not
use `tokio` when using the blocking client.

graph-rs-sdk = "1.1.0"
graph-rs-sdk = "1.1.1"

#### Example
use graph_rs_sdk::*;
Expand Down
137 changes: 137 additions & 0 deletions examples/oauth/device_code.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
use graph_rs_sdk::oauth::{AccessToken, OAuth};
use graph_rs_sdk::GraphResult;
use std::time::Duration;

// https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code

// Update the client id with your own.
fn get_oauth() -> OAuth {
let client_id = "CLIENT_ID";
let mut oauth = OAuth::new();

oauth
.client_id(client_id)
.authorize_url("https://login.microsoftonline.com/common/oauth2/v2.0/devicecode")
.refresh_token_url("https://login.microsoftonline.com/common/oauth2/v2.0/token")
.access_token_url("https://login.microsoftonline.com/common/oauth2/v2.0/token")
.add_scope("files.read")
.add_scope("offline_access");

oauth
}

// When polling to wait on the user to enter a device code you should check the errors
// so that you know what to do next.
//
// authorization_pending: The user hasn't finished authenticating, but hasn't canceled the flow. Repeat the request after at least interval seconds.
// authorization_declined: The end user denied the authorization request. Stop polling and revert to an unauthenticated state.
// bad_verification_code: The device_code sent to the /token endpoint wasn't recognized. Verify that the client is sending the correct device_code in the request.
// expired_token: Value of expires_in has been exceeded and authentication is no longer possible with device_code. Stop polling and revert to an unauthenticated state.
async fn poll_for_access_token(
device_code: &str,
interval: u64,
message: &str,
) -> GraphResult<serde_json::Value> {
let mut oauth = get_oauth();
oauth.device_code(device_code);

let mut request = oauth.build_async().device_code();
let response = request.access_token().send().await?;

println!("{response:#?}");

let status = response.status();

let body: serde_json::Value = response.json().await?;
println!("{body:#?}");

if !status.is_success() {
loop {
// Wait the amount of seconds that interval is.
std::thread::sleep(Duration::from_secs(interval));

let response = request.access_token().send().await?;

let status = response.status();
println!("{response:#?}");

let body: serde_json::Value = response.json().await?;
println!("{body:#?}");

if status.is_success() {
return Ok(body);
} else {
let option_error = body["error"].as_str();

if let Some(error) = option_error {
match error {
"authorization_pending" => println!("Still waiting on user to sign in"),
"authorization_declined" => panic!("user declined to sign in"),
"bad_verification_code" => println!("User is lost\n{message:#?}"),
"expired_token" => panic!("token has expired - user did not sign in"),
_ => {
panic!("This isn't the error we expected: {error:#?}");
}
}
} else {
// Body should have error or we should bail.
panic!("Crap hit the fan");
}
}
}
}

Ok(body)
}

// The authorization url for device code must be https://login.microsoftonline.com/{tenant}/oauth2/v2.0/devicecode
// where tenant can be common,
#[tokio::main]
async fn main() -> GraphResult<()> {
let mut oauth = get_oauth();

let mut handler = oauth.build_async().device_code();
let response = handler.authorization().send().await?;

println!("{:#?}", response);
let json: serde_json::Value = response.json().await?;
println!("{:#?}", json);

let device_code = json["device_code"].as_str().unwrap();
let interval = json["interval"].as_u64().unwrap();
let message = json["message"].as_str().unwrap();

/*
The authorization request is a POST and a successful response body will look similar to:
Object {
"device_code": String("FABABAAEAAAD--DLA3VO7QrddgJg7WevrgJ7Czy_TDsDClt2ELoEC8ePWFs"),
"expires_in": Number(900),
"interval": Number(5),
"message": String("To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code FQK5HW3UF to authenticate."),
"user_code": String("FQK5HW3UF"),
"verification_uri": String("https://microsoft.com/devicelogin"),
}
*/

// Print the message to the user who needs to sign in:
println!("{message:#?}");

// Poll for the response to the token endpoint. This will go through once
// the user has entered the code and signed in.
let access_token_json = poll_for_access_token(device_code, interval, message).await?;
let access_token: AccessToken = serde_json::from_value(access_token_json)?;
println!("{access_token:#?}");

// Get a refresh token. First pass the access token to the oauth instance.
oauth.access_token(access_token);
let mut handler = oauth.build_async().device_code();

let response = handler.refresh_token().send().await?;
println!("{response:#?}");

let body: serde_json::Value = response.json().await?;
println!("{body:#?}");

Ok(())
}
1 change: 1 addition & 0 deletions examples/oauth/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ mod auth_code_grant;
mod auth_code_grant_pkce;
mod client_credentials;
mod code_flow;
mod device_code;
mod implicit_grant;
mod is_access_token_expired;
mod logout;
Expand Down
7 changes: 6 additions & 1 deletion graph-oauth/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "graph-oauth"
version = "1.0.0"
version = "1.0.2"
authors = ["sreeise"]
edition = "2021"
license = "MIT"
Expand All @@ -25,3 +25,8 @@ url = "2"
webbrowser = "0.8.7"

graph-error = { path = "../graph-error" }

[features]
default = ["native-tls"]
native-tls = ["reqwest/native-tls"]
rustls-tls = ["reqwest/rustls-tls"]
14 changes: 11 additions & 3 deletions graph-oauth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,25 @@ for Microsoft Identity Platform or by using [graph-rs-sdk](https://crates.io/cra
For async:

```toml
graph-oauth = "1.0.0"
graph-oauth = "1.0.2"
tokio = { version = "1.25.0", features = ["full"] }
```

For blocking:

```toml
graph-oauth = "1.0.0"
graph-oauth = "1.0.2"
```

See the project on [GitHub](https://github.com/sreeise/graph-rs-sdk).
### Feature Flags

- `native-tls`: Use the `native-tls` TLS backend (OpenSSL on *nix, SChannel on Windows, Secure Transport on macOS).
- `rustls-tls`: Use the `rustls-tls` TLS backend (cross-platform backend, only supports TLS 1.2 and 1.3).

Default features: `default=["native-tls"]`

These features enable the native-tls and rustls-tls features in the reqwest crate.
For more info see the [reqwest](https://crates.io/crates/reqwest) crate.

### Supported Authorization Flows

Expand Down
Loading

0 comments on commit a6e8c9b

Please sign in to comment.