├── .editorconfig ├── .gitignore ├── .travis.yml ├── Cargo.toml ├── LICENSE.md ├── README.md ├── examples └── rocket.rs └── src ├── bridge ├── hyper.rs └── mod.rs ├── constants.rs ├── error.rs ├── lib.rs ├── model.rs ├── scope.rs └── utils.rs /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = space 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [*.rs] 11 | indent_size = 4 12 | 13 | [*.md] 14 | indent_size = 4 15 | trim_trailing_whitespace = false 16 | 17 | [*.yml] 18 | indent_size = 2 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # editor 2 | .idea/ 3 | .vscode/ 4 | 5 | target/ 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - stable 4 | - beta 5 | - nightly 6 | 7 | sudo: false 8 | os: 9 | - linux 10 | - osx 11 | 12 | cache: 13 | cargo: true 14 | 15 | script: 16 | - cargo test 17 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Zeyla Hellyer "] 3 | description = "Serenity ecosystem oauth support for the Discord API." 4 | documentation = "https://docs.rs/serenity-oauth" 5 | homepage = "https://github.com/serenity-rs/oauth" 6 | keywords = ["discord", "oauth", "serenity"] 7 | license = "ISC" 8 | name = "serenity-oauth" 9 | readme = "README.md" 10 | repository = "https://github.com/serenity-rs/oauth.git" 11 | version = "0.1.0" 12 | 13 | [dependencies] 14 | hyper = "~0.10" 15 | percent-encoding = "^1.0" 16 | serde = "^1.0" 17 | serde_derive = "^1.0" 18 | serde_json = "^1.0" 19 | serde_urlencoded = "~0.5" 20 | serenity-model = { git = "https://github.com/serenity-rs/model" } 21 | 22 | [dev-dependencies] 23 | hyper = "~0.10" 24 | hyper-native-tls = "~0.2" 25 | rocket = "~0.3" 26 | rocket_codegen = "~0.3" 27 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ISC License (ISC) 2 | 3 | Copyright (c) 2017, Zeyla Hellyer 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any purpose 6 | with or without fee is hereby granted, provided that the above copyright notice 7 | and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 11 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 13 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 14 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 15 | THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # serenity-oauth 2 | 3 | `serenity-oauth` is a collection of HTTP library support bridges for 4 | interacting with the OAuth2 API that Discord uses. 5 | 6 | It includes support for sending code exchange requests and refresh token 7 | requests. 8 | 9 | Included are models in the `model` directory that represent request bodies 10 | and response bodies. The `Scope` enum represents possible OAuth2 scopes 11 | that can be granted. 12 | 13 | In the `utils` module, functions to produce authorization URLs are 14 | available. For example, `utils::bot_authorization_url` can be used to 15 | produce a URL that can be used to redirect users to authorize an application 16 | with the `Scope::Bot` scope. 17 | 18 | ### Installation 19 | 20 | Add the following to your `Cargo.toml`: 21 | 22 | ```toml 23 | [dependencies] 24 | serenity-oauth = { git = "https://github.com/serenity-rs/oauth" } 25 | ``` 26 | 27 | And then the following to your `main.rs` or `lib.rs`: 28 | 29 | ```rust 30 | extern crate serenity_oauth; 31 | ``` 32 | 33 | ### Examples 34 | 35 | For an example of how to use this in a real-world program, see the [`examples`] 36 | directory. 37 | 38 | ### License 39 | 40 | This project is licensed under [ISC][license]. 41 | 42 | [license]: https://github.com/serenity-rs/oauth/blob/master/LICENSE.md 43 | [`examples`]: https://github.com/serenity-rs/oauth/tree/master/examples 44 | -------------------------------------------------------------------------------- /examples/rocket.rs: -------------------------------------------------------------------------------- 1 | //! This is a sample program for running a Rocket.rs server. This redirects 2 | //! users to Discord's authorization page, requesting the `identity` scope. 3 | //! 4 | //! Once they have authorized, it will take the `code` given and then exchange 5 | //! it for an access token, which can be used to access the user's identity. 6 | //! 7 | //! This example requires the following environment variables, both available 8 | //! from your Discord application's settings: 9 | //! 10 | //! - `DISCORD_CLIENT_ID` 11 | //! - `DISCORD_CLIENT_SECRET` 12 | //! 13 | //! You will also need to register a redirect URI. Running this locally would 14 | //! cause the redirect URI to be `http://localhost:8000` for example. This can 15 | //! be registered in your application's settings. 16 | //! 17 | //! Example of how to run this: 18 | //! 19 | //! `$ git clone https://github.com/serenity-rs/oauth` 20 | //! `$ cd oauth` 21 | //! `$ DISCORD_CLIENT_SECRET=my_secret DISCORD_CLIENT_ID=my_client_id cargo run --example rocket` 22 | 23 | #![feature(custom_derive, plugin)] 24 | #![plugin(rocket_codegen)] 25 | 26 | extern crate hyper; 27 | extern crate hyper_native_tls; 28 | extern crate serenity_oauth; 29 | extern crate rocket; 30 | 31 | use hyper::net::HttpsConnector; 32 | use hyper::Client as HyperClient; 33 | use hyper_native_tls::NativeTlsClient; 34 | use rocket::response::Redirect; 35 | use serenity_oauth::model::AccessTokenExchangeRequest; 36 | use serenity_oauth::{DiscordOAuthHyperRequester, Scope}; 37 | use std::env; 38 | use std::error::Error; 39 | 40 | #[derive(Debug, FromForm)] 41 | struct Params { 42 | code: String, 43 | } 44 | 45 | fn get_client_id() -> u64 { 46 | env::var("DISCORD_CLIENT_ID") 47 | .expect("No DISCORD_CLIENT_ID present") 48 | .parse::() 49 | .expect("Error parsing DISCORD_CLIENT_ID into u64") 50 | } 51 | 52 | fn get_client_secret() -> String { 53 | env::var("DISCORD_CLIENT_SECRET") 54 | .expect("No DISCORD_CLIENT_SECRET present") 55 | } 56 | 57 | #[get("/callback?")] 58 | fn get_callback(params: Params) -> Result> { 59 | // Exchange the code for an access token. 60 | let ssl = NativeTlsClient::new()?; 61 | let connector = HttpsConnector::new(ssl); 62 | let client = HyperClient::with_connector(connector); 63 | 64 | let response = client.exchange_code(&AccessTokenExchangeRequest::new( 65 | get_client_id(), 66 | get_client_secret(), 67 | params.code, 68 | "http://localhost:8000/callback", 69 | ))?; 70 | 71 | Ok(format!("The user's access token is: {}", response.access_token)) 72 | } 73 | 74 | #[get("/")] 75 | fn get_redirect() -> Redirect { 76 | // Although this example does not use a state, you _should always_ use one 77 | // in production for security purposes. 78 | let url = serenity_oauth::utils::authorization_code_grant_url( 79 | get_client_id(), 80 | &[Scope::Identify], 81 | None, 82 | "http://localhost:8000/callback", 83 | ); 84 | 85 | Redirect::to(&url) 86 | } 87 | 88 | fn main() { 89 | rocket::ignite() 90 | .mount("/", routes![ 91 | get_callback, 92 | get_redirect, 93 | ]).launch(); 94 | } 95 | -------------------------------------------------------------------------------- /src/bridge/hyper.rs: -------------------------------------------------------------------------------- 1 | //! Bridged support for the `hyper` HTTP client. 2 | 3 | use hyper::client::{Body, Client as HyperClient}; 4 | use hyper::header::ContentType; 5 | use serde_json; 6 | use serde_urlencoded; 7 | use ::constants::BASE_TOKEN_URI; 8 | use ::model::{ 9 | AccessTokenExchangeRequest, 10 | AccessTokenResponse, 11 | RefreshTokenRequest, 12 | }; 13 | use ::Result; 14 | 15 | /// A trait used that implements methods for interacting with Discord's OAuth2 16 | /// API on Hyper's client. 17 | /// 18 | /// # Examples 19 | /// 20 | /// Bringing in the trait and creating a client. Since the trait is in scope, 21 | /// the instance of hyper's Client will have those methods available: 22 | /// 23 | /// ```rust,no_run 24 | /// extern crate hyper; 25 | /// extern crate serenity_oauth; 26 | /// 27 | /// # fn main() { 28 | /// use hyper::Client; 29 | /// 30 | /// let client = Client::new(); 31 | /// 32 | /// // At this point, the methods defined by the trait are not in scope. By 33 | /// // using the trait, they will be. 34 | /// use serenity_oauth::DiscordOAuthHyperRequester; 35 | /// 36 | /// // The methods defined by `DiscordOAuthHyperRequester` are now in scope and 37 | /// // implemented on the instance of hyper's `Client`. 38 | /// # } 39 | /// ``` 40 | /// 41 | /// For examples of how to use the trait with the Client, refer to the trait's 42 | /// methods. 43 | pub trait DiscordOAuthHyperRequester { 44 | /// Exchanges a code for the user's access token. 45 | /// 46 | /// # Examples 47 | /// 48 | /// Exchange a code for an access token: 49 | /// 50 | /// ```rust,no_run 51 | /// extern crate hyper; 52 | /// extern crate serenity_oauth; 53 | /// 54 | /// # use std::error::Error; 55 | /// # 56 | /// # fn try_main() -> Result<(), Box> { 57 | /// use hyper::Client; 58 | /// use serenity_oauth::model::AccessTokenExchangeRequest; 59 | /// use serenity_oauth::DiscordOAuthHyperRequester; 60 | /// 61 | /// let request_data = AccessTokenExchangeRequest::new( 62 | /// 249608697955745802, 63 | /// "dd99opUAgs7SQEtk2kdRrTMU5zagR2a4", 64 | /// "user code here", 65 | /// "https://myapplication.website", 66 | /// ); 67 | /// 68 | /// let client = Client::new(); 69 | /// let response = client.exchange_code(&request_data)?; 70 | /// 71 | /// println!("Access token: {}", response.access_token); 72 | /// # Ok(()) 73 | /// # } 74 | /// # 75 | /// # fn main() { 76 | /// # try_main().unwrap(); 77 | /// # } 78 | /// ``` 79 | fn exchange_code(&self, request: &AccessTokenExchangeRequest) 80 | -> Result; 81 | 82 | /// Exchanges a refresh token, returning a new refresh token and fresh 83 | /// access token. 84 | /// 85 | /// # Examples 86 | /// 87 | /// Exchange a refresh token: 88 | /// 89 | /// ```rust,no_run 90 | /// extern crate hyper; 91 | /// extern crate serenity_oauth; 92 | /// 93 | /// # use std::error::Error; 94 | /// # 95 | /// # fn try_main() -> Result<(), Box> { 96 | /// use hyper::Client; 97 | /// use serenity_oauth::model::RefreshTokenRequest; 98 | /// use serenity_oauth::DiscordOAuthHyperRequester; 99 | /// 100 | /// let request_data = RefreshTokenRequest::new( 101 | /// 249608697955745802, 102 | /// "dd99opUAgs7SQEtk2kdRrTMU5zagR2a4", 103 | /// "user code here", 104 | /// "https://myapplication.website", 105 | /// ); 106 | /// 107 | /// let client = Client::new(); 108 | /// let response = client.exchange_refresh_token(&request_data)?; 109 | /// 110 | /// println!("Fresh access token: {}", response.access_token); 111 | /// # Ok(()) 112 | /// # } 113 | /// # 114 | /// # fn main() { 115 | /// # try_main().unwrap(); 116 | /// # } 117 | /// ``` 118 | fn exchange_refresh_token(&self, request: &RefreshTokenRequest) 119 | -> Result; 120 | } 121 | 122 | impl DiscordOAuthHyperRequester for HyperClient { 123 | fn exchange_code(&self, request: &AccessTokenExchangeRequest) 124 | -> Result { 125 | let body = serde_urlencoded::to_string(request)?; 126 | 127 | let response = self.post(BASE_TOKEN_URI) 128 | .header(ContentType::form_url_encoded()) 129 | .body(Body::BufBody(body.as_bytes(), body.len())) 130 | .send()?; 131 | 132 | serde_json::from_reader(response).map_err(From::from) 133 | } 134 | 135 | fn exchange_refresh_token(&self, request: &RefreshTokenRequest) 136 | -> Result { 137 | let body = serde_json::to_string(request)?; 138 | 139 | let response = self.post(BASE_TOKEN_URI) 140 | .body(Body::BufBody(body.as_bytes(), body.len())) 141 | .send()?; 142 | 143 | serde_json::from_reader(response).map_err(From::from) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/bridge/mod.rs: -------------------------------------------------------------------------------- 1 | //! A module containing bridges to HTTP clients. 2 | //! 3 | //! This contains traits implemented on HTTP clients, as well as oneshot 4 | //! functions that create one-off clients for ease of use. 5 | 6 | pub mod hyper; 7 | -------------------------------------------------------------------------------- /src/constants.rs: -------------------------------------------------------------------------------- 1 | //! A set of constants around the OAuth2 API. 2 | 3 | /// The base authorization URI, used for authorizing an application. 4 | pub const BASE_AUTHORIZE_URI: &str = "https://discordapp.com/api/oauth2/authorize"; 5 | /// The revocation URL, used to revoke an access token. 6 | pub const BASE_REVOKE_URI: &str = "https://discordapp.com/api/oauth2/revoke"; 7 | /// The token URI, used for exchanging a refresh token for a fresh access token 8 | /// and new refresh token. 9 | pub const BASE_TOKEN_URI: &str = "https://discordapp.com/api/oauth2/token"; 10 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use hyper::Error as HyperError; 2 | use serde_json::Error as JsonError; 3 | use serde_urlencoded::ser::Error as UrlEncodeError; 4 | use std::error::Error as StdError; 5 | use std::fmt::{Display, Formatter, Result as FmtResult}; 6 | use std::result::Result as StdResult; 7 | 8 | /// Result type used throughout the library's public result functions. 9 | pub type Result = StdResult; 10 | 11 | /// Standard error enum used to wrap different potential error types. 12 | #[derive(Debug)] 13 | pub enum Error { 14 | /// An error from the `hyper` crate. 15 | Hyper(HyperError), 16 | /// An error from the `serde_json` crate. 17 | Json(JsonError), 18 | /// An error from the `serde_urlencoded` crate. 19 | UrlEncode(UrlEncodeError), 20 | } 21 | 22 | impl From for Error { 23 | fn from(err: HyperError) -> Self { 24 | Error::Hyper(err) 25 | } 26 | } 27 | 28 | impl From for Error { 29 | fn from(err: JsonError) -> Self { 30 | Error::Json(err) 31 | } 32 | } 33 | 34 | impl From for Error { 35 | fn from(err: UrlEncodeError) -> Self { 36 | Error::UrlEncode(err) 37 | } 38 | } 39 | 40 | impl Display for Error { 41 | fn fmt(&self, f: &mut Formatter) -> FmtResult { 42 | f.write_str(self.description()) 43 | } 44 | } 45 | 46 | impl StdError for Error { 47 | fn description(&self) -> &str { 48 | match *self { 49 | Error::Hyper(ref inner) => inner.description(), 50 | Error::Json(ref inner) => inner.description(), 51 | Error::UrlEncode(ref inner) => inner.description(), 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # serenity-oauth 2 | //! 3 | //! `serenity-oauth` is a collection of HTTP library support bridges for 4 | //! interacting with the OAuth2 API that Discord uses. 5 | //! 6 | //! It includes support for sending code exchange requests and refresh token 7 | //! requests. 8 | //! 9 | //! Included are models in the [`model`] directory that represent request bodies 10 | //! and response bodies. The [`Scope`] enum represents possible OAuth2 scopes 11 | //! that can be granted. 12 | //! 13 | //! In the [`utils`] module, functions to produce authorization URLs are 14 | //! available. For example, [`utils::bot_authorization_url`] can be used to 15 | //! produce a URL that can be used to redirect users to authorize an application 16 | //! with the [`Scope::Bot`] scope. 17 | //! 18 | //! [`Scope`]: enum.Scope.html 19 | //! [`Scope::Bot`]: enum.Scope.html#variant.Bot 20 | //! [`model`]: model/ 21 | //! [`utils`]: utils/ 22 | //! [`utils::bot_authorization_url`]: utils/fn.bot_authorization_url.html 23 | 24 | #![deny(missing_docs)] 25 | 26 | #[macro_use] extern crate serde_derive; 27 | 28 | extern crate hyper; 29 | extern crate percent_encoding; 30 | extern crate serde; 31 | extern crate serde_json; 32 | extern crate serde_urlencoded; 33 | extern crate serenity_model; 34 | 35 | pub mod bridge; 36 | pub mod constants; 37 | pub mod model; 38 | pub mod utils; 39 | 40 | mod error; 41 | mod scope; 42 | 43 | pub use bridge::hyper::DiscordOAuthHyperRequester; 44 | pub use error::{Error, Result}; 45 | pub use scope::Scope; 46 | -------------------------------------------------------------------------------- /src/model.rs: -------------------------------------------------------------------------------- 1 | //! A collection of models that can be deserialized from response bodies and 2 | //! serialized into request bodies. 3 | 4 | use serenity_model::{PartialGuild, Webhook}; 5 | 6 | /// Structure of data used as the body of a request to exchange the [`code`] for 7 | /// an access token. 8 | /// 9 | /// [`code`]: #structfield.code 10 | #[derive(Clone, Debug, Deserialize, Serialize)] 11 | pub struct AccessTokenExchangeRequest { 12 | /// Your application's client ID. 13 | pub client_id: u64, 14 | /// Your application's client secret. 15 | pub client_secret: String, 16 | /// The code in the query parameters to your redirect URI. 17 | pub code: String, 18 | /// The type of grant. 19 | /// 20 | /// Must be set to `authorization_code`. 21 | /// 22 | /// If using [`AccessTokenExchangeRequest::new`], this will automatically be 23 | /// set for you. 24 | pub grant_type: String, 25 | /// Your redirect URI. 26 | pub redirect_uri: String, 27 | } 28 | 29 | impl AccessTokenExchangeRequest { 30 | /// Creates a new request body for exchanging a code for an access token. 31 | /// 32 | /// # Examples 33 | /// 34 | /// Create a new request and assert that the grant type is correct: 35 | /// 36 | /// ```rust 37 | /// use serenity_oauth::model::AccessTokenExchangeRequest; 38 | /// 39 | /// let request = AccessTokenExchangeRequest::new( 40 | /// 249608697955745802, 41 | /// "dd99opUAgs7SQEtk2kdRrTMU5zagR2a4", 42 | /// "user code here", 43 | /// "https://myapplication.website", 44 | /// ); 45 | /// 46 | /// assert_eq!(request.grant_type, "authorization_code"); 47 | /// ``` 48 | pub fn new( 49 | client_id: u64, 50 | client_secret: S, 51 | code: T, 52 | redirect_uri: U, 53 | ) -> Self where S: Into, T: Into, U: Into { 54 | Self { 55 | client_secret: client_secret.into(), 56 | code: code.into(), 57 | grant_type: "authorization_code".to_owned(), 58 | redirect_uri: redirect_uri.into(), 59 | client_id, 60 | } 61 | } 62 | } 63 | 64 | /// Response data containing a new access token and refresh token. 65 | /// 66 | /// This can be received when either: 67 | /// 68 | /// 1. exchanging a code for an access token; 69 | /// 2. exchanging a refresh token for a fresh access token. 70 | #[derive(Clone, Debug, Deserialize, Serialize)] 71 | pub struct AccessTokenResponse { 72 | /// The user's access token. 73 | pub access_token: String, 74 | /// The number of seconds until the access token expires. 75 | pub expires_in: u64, 76 | /// The refresh token to use when the access token expires. 77 | pub refresh_token: String, 78 | /// The scope that is granted. 79 | pub scope: String, 80 | /// The type of token received. 81 | pub token_type: String, 82 | } 83 | 84 | /// Response data containing an access token, but without a refresh token. 85 | #[derive(Clone, Debug, Deserialize, Serialize)] 86 | pub struct ClientCredentialsAccessTokenResponse { 87 | /// The user's access token. 88 | pub access_token: String, 89 | /// The number of seconds until the access token expires. 90 | pub expires_in: u64, 91 | /// The scope that is granted. 92 | pub scope: String, 93 | /// The type of token received. 94 | pub token_type: String, 95 | } 96 | 97 | /// An extended [`Scope::Bot`] authorization flow. 98 | /// 99 | /// This will authorize the application as a bot into a user's selected guild, 100 | /// as well as granting additional scopes. 101 | /// 102 | /// [`Scope::Bot`]: ../enum.Scope.html#variant.Bot 103 | #[derive(Clone, Debug, Deserialize)] 104 | pub struct ExtendedBotAuthorizationResponse { 105 | /// The user's access token. 106 | pub access_token: String, 107 | /// The number of seconds until the access token expires. 108 | pub expires_in: u64, 109 | /// Partial guild data that the application was authorized into. 110 | pub guild: PartialGuild, 111 | /// The refresh token to use when the access token expires. 112 | pub refresh_token: String, 113 | /// The scope that is granted. 114 | pub scope: String, 115 | /// The type of token received. 116 | pub token_type: String, 117 | } 118 | 119 | /// Request for exchanging a refresh token for a new access token. 120 | #[derive(Clone, Debug, Deserialize, Serialize)] 121 | pub struct RefreshTokenRequest { 122 | /// Your application's client ID. 123 | pub client_id: u64, 124 | /// Your application's client secret. 125 | pub client_secret: String, 126 | /// The type of grant. 127 | /// 128 | /// Must be set to `refresh_token`. 129 | /// 130 | /// If using [`RefreshTokenRequest::new`], this will automatically be 131 | /// set for you. 132 | pub grant_type: String, 133 | /// Your redirect URI. 134 | pub redirect_uri: String, 135 | /// The user's refresh token. 136 | pub refresh_token: String, 137 | } 138 | 139 | impl RefreshTokenRequest { 140 | /// Creates a new request body for refreshing an access token using a 141 | /// refresh token. 142 | /// 143 | /// # Examples 144 | /// 145 | /// Create a new request and assert that the grant type is correct: 146 | /// 147 | /// ```rust 148 | /// use serenity_oauth::model::RefreshTokenRequest; 149 | /// 150 | /// let request = RefreshTokenRequest::new( 151 | /// 249608697955745802, 152 | /// "dd99opUAgs7SQEtk2kdRrTMU5zagR2a4", 153 | /// "user code here", 154 | /// "https://myapplication.website", 155 | /// ); 156 | /// 157 | /// assert_eq!(request.grant_type, "refresh_token"); 158 | /// ``` 159 | pub fn new( 160 | client_id: u64, 161 | client_secret: S, 162 | redirect_uri: T, 163 | refresh_token: U, 164 | ) -> Self where S: Into, T: Into, U: Into { 165 | Self { 166 | client_secret: client_secret.into(), 167 | grant_type: "refresh_token".to_owned(), 168 | redirect_uri: redirect_uri.into(), 169 | refresh_token: refresh_token.into(), 170 | client_id, 171 | } 172 | } 173 | } 174 | 175 | /// The response data from a successful trading of a code for an access token 176 | /// after authorization of [`Scope::WebhookIncoming`]. 177 | /// 178 | /// You should store [`webhook`]'s `id` and `token` structfields. 179 | /// 180 | /// [`Scope::WebhookIncoming`]: ../enum.Scope.html#variant.WebhookIncoming 181 | /// [`webhook`]: #structfield.webhook 182 | #[derive(Clone, Debug, Deserialize)] 183 | pub struct WebhookTokenResponse { 184 | /// The user's access token. 185 | pub access_token: String, 186 | /// The number of seconds until the access token expires. 187 | pub expires_in: u64, 188 | /// The refresh token to use when the access token expires. 189 | pub refresh_token: String, 190 | /// The scope that is granted. 191 | pub scope: String, 192 | /// The type of token received. 193 | pub token_type: String, 194 | /// Information about the webhook created. 195 | pub webhook: Webhook, 196 | } 197 | -------------------------------------------------------------------------------- /src/scope.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter, Result as FmtResult}; 2 | 3 | /// A Discord OAuth2 scope that can be granted. 4 | /// 5 | /// If you require a scope that is not registered here, use [`Scope::Other`] and 6 | /// notify the library developers about the missing scope. 7 | /// 8 | /// **Note**: The [`Scope::Bot`] and [`Scope::GuildsJoin`] scopes require you to 9 | /// have a bot account linked to your application. Also, in order to add a user 10 | /// to a guild, your bot has to already belong in that guild. 11 | /// 12 | /// [`Scope::Bot`]: #variant.Bot 13 | /// [`Scope::GuildsJoin`]: #variant.GuildsJoin 14 | /// [`Scope::Other`]: #variant.Other 15 | #[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] 16 | pub enum Scope { 17 | /// For OAuth2 bots, this puts the bot in the user's selected guild by 18 | /// default. 19 | Bot, 20 | /// Allows the `/users/@me/connections` API endpoint to return linked 21 | /// third-party accounts. 22 | Connections, 23 | /// Enables the `/users/@me` API endpoint to return an `email` field. 24 | Email, 25 | /// Allows the `/users/@me` API endpoint, without the `email` field. 26 | Identify, 27 | /// Allows the `/users/@me/guilds` API endpoint to return basic information 28 | /// about all of a user's guilds. 29 | Guilds, 30 | /// Allows the `/invites/{code}` API endpoint to be used for joining users 31 | /// to a guild. 32 | GuildsJoin, 33 | /// Allows your application to join users to a group DM. 34 | GdmJoin, 35 | /// For local RPC server API access, this allows you to read messages from 36 | /// all cliuent channels. 37 | /// 38 | /// This is otherwise restricted to channels/guilds your application 39 | /// creates. 40 | MessagesRead, 41 | /// For local RPC server access, this allows you to control a user's local 42 | /// Discord client. 43 | Rpc, 44 | /// For local RPC server API access, this allows you to access the API as 45 | /// the local user. 46 | RpcApi, 47 | /// For local RPC server API access, this allows you to receive 48 | /// notifications pushed out to the user. 49 | RpcNotificationsRead, 50 | /// This generates a webhook that is returned in the OAuth token response 51 | /// for authorization code grants. 52 | WebhookIncoming, 53 | /// A scope that does not have a matching enum variant. 54 | Other(String), 55 | } 56 | 57 | impl Display for Scope { 58 | fn fmt(&self, f: &mut Formatter) -> FmtResult { 59 | use self::Scope::*; 60 | 61 | f.write_str(match *self { 62 | Bot => "bot", 63 | Connections => "connections", 64 | Email => "email", 65 | Identify => "identify", 66 | Guilds => "guilds", 67 | GuildsJoin => "guilds.join", 68 | GdmJoin => "gdm.join", 69 | MessagesRead => "messages.read", 70 | Rpc => "rpc", 71 | RpcApi => "rpc.api", 72 | RpcNotificationsRead => "rpc.notifications.read", 73 | WebhookIncoming => "webhook.incoming", 74 | Other(ref inner) => inner, 75 | }) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | //! A collection of functions for use with the OAuth2 API. 2 | //! 3 | //! This includes functions for easily generating URLs to redirect users to for 4 | //! authorization. 5 | 6 | pub use serenity_model::Permissions; 7 | 8 | use constants::BASE_AUTHORIZE_URI; 9 | use percent_encoding; 10 | use super::Scope; 11 | use std::fmt::Write; 12 | 13 | /// Creates a URL for a simple bot authorization flow. 14 | /// 15 | /// This is a special, 16 | /// server-less and callback-less OAuth2 flow that makes it 17 | /// easy for users to add bots to guilds. 18 | /// 19 | /// # Examples 20 | /// 21 | /// Create an authorization URL for a bot requiring the "Add Reactions" and 22 | /// "Send Messages" permissions: 23 | /// 24 | /// ```rust 25 | /// extern crate serenity_model; 26 | /// extern crate serenity_oauth; 27 | /// 28 | /// # fn main() { 29 | /// use serenity_model::Permissions; 30 | /// 31 | /// let client_id = 249608697955745802; 32 | /// let required = Permissions::ADD_REACTIONS | Permissions::SEND_MESSAGES; 33 | /// let url = serenity_oauth::utils::bot_authorization_url(client_id, required); 34 | /// 35 | /// // Assert that the expected URL is correct. 36 | /// let expected = "https://discordapp.com/api/oauth2/authorize?client_id=249608697955745802&scope=bot&permissions=2112"; 37 | /// assert_eq!(url, expected); 38 | /// # } 39 | /// ``` 40 | pub fn bot_authorization_url(client_id: u64, permissions: Permissions) 41 | -> String { 42 | format!( 43 | "{}?client_id={}&scope=bot&permissions={}", 44 | BASE_AUTHORIZE_URI, 45 | client_id, 46 | permissions.bits(), 47 | ) 48 | } 49 | 50 | /// Creates a URL for an authorization code grant. 51 | /// 52 | /// This will create a URL to redirect the user to, requesting the given scopes 53 | /// for your client ID. 54 | /// 55 | /// The given `redirect_uri` will automatically be URL encoded. 56 | /// 57 | /// A state _should_ be passed, as recommended by RFC 6749. It is a unique 58 | /// identifier for the user's request. When Discord redirects the user to the 59 | /// given redirect URI, it will append a `state` parameter to the query. It will 60 | /// match the state that you have recorded for that user. If it does not, there 61 | /// was likely a request interception. 62 | /// 63 | /// As well as the callback URL having the same `state` appended in the query 64 | /// parameters, this will also append a `code`. 65 | /// 66 | /// # Examples 67 | /// 68 | /// Produce an authorization code grant URL for your client, requiring the 69 | /// [`Scope::Identify`] and [`Scope::GuildsJoin`] scopes, and an example of a 70 | /// state: 71 | /// 72 | /// **Note**: Please randomly generate this using a crate like `rand`. 73 | /// 74 | /// ```rust 75 | /// use serenity_oauth::Scope; 76 | /// 77 | /// let client_id = 249608697955745802; 78 | /// let scopes = [Scope::GuildsJoin, Scope::Identify]; 79 | /// let state = "15773059ghq9183habn"; 80 | /// let redirect_uri = "https://myapplication.website"; 81 | /// 82 | /// let url = serenity_oauth::utils::authorization_code_grant_url( 83 | /// client_id, 84 | /// &scopes, 85 | /// Some(state), 86 | /// redirect_uri, 87 | /// ); 88 | /// 89 | /// // Assert that the URL is correct. 90 | /// let expected = "https://discordapp.com/api/oauth2/authorize?response_type=code&client_id=249608697955745802&redirect_uri=https%3A%2F%2Fmyapplication.website&scope=guilds.join%20identify&state=15773059ghq9183habn"; 91 | /// assert_eq!(url, expected); 92 | /// ``` 93 | /// 94 | /// [`Scope::GuildsJoin`]: enum.Scope.html#variant.GuildsJoin 95 | /// [`Scope::Identify`]: enum.Scope.html#variant.Identify 96 | pub fn authorization_code_grant_url( 97 | client_id: u64, 98 | scopes: &[Scope], 99 | state: Option<&str>, 100 | redirect_uri: &str, 101 | ) -> String { 102 | let mut base = String::from(BASE_AUTHORIZE_URI); 103 | let uri = percent_encoding::percent_encode( 104 | redirect_uri.as_bytes(), 105 | percent_encoding::USERINFO_ENCODE_SET, 106 | ); 107 | 108 | let _ = write!( 109 | base, 110 | "?response_type=code&client_id={}&redirect_uri={}&scope=", 111 | client_id, 112 | uri, 113 | ); 114 | 115 | let scope_count = scopes.len(); 116 | 117 | for (i, scope) in scopes.iter().enumerate() { 118 | let _ = write!(base, "{}", scope); 119 | 120 | if i + 1 < scope_count { 121 | base.push_str("%20"); 122 | } 123 | } 124 | 125 | if let Some(state) = state { 126 | let _ = write!(base, "&state={}", state); 127 | } 128 | 129 | base 130 | } 131 | --------------------------------------------------------------------------------