Skip to content

Commit

Permalink
Track Client endpoints statically via typestates
Browse files Browse the repository at this point in the history
Currently, the authorization endpoint is required, and all other
endpoints are optional. This both causes problems for authentication flows
that don't require an authorization endpoint (and for which the server may
not implement one; see #135), and introduces fallibility into methods that
depend on endpoints having been set previously.

This change makes all endpoints optional, and each endpoint has a
corresponding setter method as part of the Builder Pattern. Each endpoint
has a corresponding const generic parameter within the `Client` that tracks
whether that endpoint has been set. Each method that depends on an endpoint
is implemented only for `Client` instances that have previously called the
corresponding setter, which is enforced at compile time.

BREAKING CHANGE: The `Client::new()` method now only accepts a client ID.
The client secret, authorization endpoint, and token endpoint have been
moved to `set_client_secret`, `set_auth_url`, and `set_token_url` methods,
respectively. Also, the additional const generics added to `Client` and
`BasicClient` will need to be specified at each call site that specifies
any of the generic parameters.
  • Loading branch information
ramosbugs committed Feb 21, 2024
1 parent 96c6f9b commit 1d1f4d1
Show file tree
Hide file tree
Showing 12 changed files with 797 additions and 466 deletions.
20 changes: 9 additions & 11 deletions examples/github.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,15 @@ fn main() {
.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,
Some(token_url),
)
// This example will be running its own server at localhost:8080.
// See below for the server implementation.
.set_redirect_uri(
RedirectUrl::new("http://localhost:8080".to_string()).expect("Invalid redirect URL"),
);
let client = BasicClient::new(github_client_id)
.set_client_secret(github_client_secret)
.set_auth_url(auth_url)
.set_token_url(token_url)
// This example will be running its own server at localhost:8080.
// See below for the server implementation.
.set_redirect_uri(
RedirectUrl::new("http://localhost:8080".to_string()).expect("Invalid redirect URL"),
);

// Generate the authorization URL to which we'll redirect the user.
let (authorize_url, csrf_state) = client
Expand Down
20 changes: 9 additions & 11 deletions examples/github_async.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,15 @@ async fn main() {
.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,
Some(token_url),
)
// This example will be running its own server at localhost:8080.
// See below for the server implementation.
.set_redirect_uri(
RedirectUrl::new("http://localhost:8080".to_string()).expect("Invalid redirect URL"),
);
let client = BasicClient::new(github_client_id)
.set_client_secret(github_client_secret)
.set_auth_url(auth_url)
.set_token_url(token_url)
// This example will be running its own server at localhost:8080.
// See below for the server implementation.
.set_redirect_uri(
RedirectUrl::new("http://localhost:8080".to_string()).expect("Invalid redirect URL"),
);

// Generate the authorization URL to which we'll redirect the user.
let (authorize_url, csrf_state) = client
Expand Down
30 changes: 14 additions & 16 deletions examples/google.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,22 +40,20 @@ fn main() {
.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,
Some(token_url),
)
// This example will be running its own server at localhost:8080.
// See below for the server implementation.
.set_redirect_uri(
RedirectUrl::new("http://localhost:8080".to_string()).expect("Invalid redirect URL"),
)
// Google supports OAuth 2.0 Token Revocation (RFC-7009)
.set_revocation_uri(
RevocationUrl::new("https://oauth2.googleapis.com/revoke".to_string())
.expect("Invalid revocation endpoint URL"),
);
let client = BasicClient::new(google_client_id)
.set_client_secret(google_client_secret)
.set_auth_url(auth_url)
.set_token_url(token_url)
// This example will be running its own server at localhost:8080.
// See below for the server implementation.
.set_redirect_uri(
RedirectUrl::new("http://localhost:8080".to_string()).expect("Invalid redirect URL"),
)
// Google supports OAuth 2.0 Token Revocation (RFC-7009)
.set_revocation_uri(
RevocationUrl::new("https://oauth2.googleapis.com/revoke".to_string())
.expect("Invalid revocation endpoint URL"),
);

// Google supports Proof Key for Code Exchange (PKCE - https://oauth.net/2/pkce/).
// Create a PKCE code verifier and SHA-256 encode it as a code challenge.
Expand Down
15 changes: 6 additions & 9 deletions examples/google_devicecode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,19 +49,16 @@ fn main() {
//
// Google's OAuth endpoint expects the client_id to be in the request body,
// so ensure that option is set.
let device_client = BasicClient::new(
google_client_id,
Some(google_client_secret),
auth_url,
Some(token_url),
)
.set_device_authorization_url(device_auth_url)
.set_auth_type(AuthType::RequestBody);
let device_client = BasicClient::new(google_client_id)
.set_client_secret(google_client_secret)
.set_auth_url(auth_url)
.set_token_url(token_url)
.set_device_authorization_url(device_auth_url)
.set_auth_type(AuthType::RequestBody);

// Request the set of codes from the Device Authorization endpoint.
let details: StoringDeviceAuthorizationResponse = device_client
.exchange_device_code()
.unwrap()
.add_scope(Scope::new("profile".to_string()))
.request(http_client)
.expect("Failed to request codes from device auth endpoint");
Expand Down
10 changes: 4 additions & 6 deletions examples/letterboxd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,10 @@ fn main() -> Result<(), anyhow::Error> {
let token_url = TokenUrl::new("https://api.letterboxd.com/api/v0/auth/token".to_string())?;

// Set up the config for the Letterboxd OAuth2 process.
let client = BasicClient::new(
letterboxd_client_id.clone(),
Some(letterboxd_client_secret.clone()),
auth_url,
Some(token_url),
);
let client = BasicClient::new(letterboxd_client_id.clone())
.set_client_secret(letterboxd_client_secret.clone())
.set_auth_url(auth_url)
.set_token_url(token_url);

// Resource Owner flow uses username and password for authentication
let letterboxd_username = ResourceOwnerUsername::new(
Expand Down
22 changes: 10 additions & 12 deletions examples/microsoft_devicecode_common_user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,19 @@ use std::error::Error;

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let device_auth_url = DeviceAuthorizationUrl::new(
"https://login.microsoftonline.com/common/oauth2/v2.0/devicecode".to_string(),
)?;
let client = BasicClient::new(
ClientId::new("client_id".to_string()),
None,
AuthUrl::new("https://login.microsoftonline.com/common/oauth2/v2.0/authorize".to_string())?,
Some(TokenUrl::new(
let client = BasicClient::new(ClientId::new("client_id".to_string()))
.set_auth_url(AuthUrl::new(
"https://login.microsoftonline.com/common/oauth2/v2.0/authorize".to_string(),
)?)
.set_token_url(TokenUrl::new(
"https://login.microsoftonline.com/common/oauth2/v2.0/token".to_string(),
)?),
)
.set_device_authorization_url(device_auth_url);
)?)
.set_device_authorization_url(DeviceAuthorizationUrl::new(
"https://login.microsoftonline.com/common/oauth2/v2.0/devicecode".to_string(),
)?);

let details: StandardDeviceAuthorizationResponse = client
.exchange_device_code()?
.exchange_device_code()
.add_scope(Scope::new("read".to_string()))
.request_async(async_http_client)
.await?;
Expand Down
24 changes: 10 additions & 14 deletions examples/microsoft_devicecode_tenant_user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,22 @@ const TENANT_ID: &str = "{tenant}";

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let device_auth_url = DeviceAuthorizationUrl::new(format!(
"https://login.microsoftonline.com/{}/oauth2/v2.0/devicecode",
TENANT_ID
))?;
let client = BasicClient::new(
ClientId::new("client_id".to_string()),
None,
AuthUrl::new(format!(
let client = BasicClient::new(ClientId::new("client_id".to_string()))
.set_auth_url(AuthUrl::new(format!(
"https://login.microsoftonline.com/{}/oauth2/v2.0/authorize",
TENANT_ID
))?,
Some(TokenUrl::new(format!(
))?)
.set_token_url(TokenUrl::new(format!(
"https://login.microsoftonline.com/{}/oauth2/v2.0/token",
TENANT_ID
))?),
)
.set_device_authorization_url(device_auth_url);
))?)
.set_device_authorization_url(DeviceAuthorizationUrl::new(format!(
"https://login.microsoftonline.com/{}/oauth2/v2.0/devicecode",
TENANT_ID
))?);

let details: StandardDeviceAuthorizationResponse = client
.exchange_device_code()?
.exchange_device_code()
.add_scope(Scope::new("read".to_string()))
.request_async(async_http_client)
.await?;
Expand Down
28 changes: 13 additions & 15 deletions examples/msgraph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,21 +49,19 @@ fn main() {
.expect("Invalid token endpoint URL");

// Set up the config for the Microsoft Graph OAuth2 process.
let client = BasicClient::new(
graph_client_id,
Some(graph_client_secret),
auth_url,
Some(token_url),
)
// Microsoft Graph requires client_id and client_secret in URL rather than
// using Basic authentication.
.set_auth_type(AuthType::RequestBody)
// This example will be running its own server at localhost:3003.
// See below for the server implementation.
.set_redirect_uri(
RedirectUrl::new("http://localhost:3003/redirect".to_string())
.expect("Invalid redirect URL"),
);
let client = BasicClient::new(graph_client_id)
.set_client_secret(graph_client_secret)
.set_auth_url(auth_url)
.set_token_url(token_url)
// Microsoft Graph requires client_id and client_secret in URL rather than
// using Basic authentication.
.set_auth_type(AuthType::RequestBody)
// This example will be running its own server at localhost:3003.
// See below for the server implementation.
.set_redirect_uri(
RedirectUrl::new("http://localhost:3003/redirect".to_string())
.expect("Invalid redirect URL"),
);

// Microsoft Graph supports Proof Key for Code Exchange (PKCE - https://oauth.net/2/pkce/).
// Create a PKCE code verifier and SHA-256 encode it as a code challenge.
Expand Down
33 changes: 21 additions & 12 deletions examples/wunderlist.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,24 @@ use std::net::TcpListener;
use std::time::Duration;

type SpecialTokenResponse = NonStandardTokenResponse<EmptyExtraTokenFields>;
type SpecialClient = Client<
type SpecialClient<
const HAS_AUTH_URL: bool,
const HAS_DEVICE_AUTH_URL: bool,
const HAS_INTROSPECTION_URL: bool,
const HAS_REVOCATION_URL: bool,
const HAS_TOKEN_URL: bool,
> = Client<
BasicErrorResponse,
SpecialTokenResponse,
BasicTokenType,
BasicTokenIntrospectionResponse,
StandardRevocableToken,
BasicRevocationErrorResponse,
HAS_AUTH_URL,
HAS_DEVICE_AUTH_URL,
HAS_INTROSPECTION_URL,
HAS_REVOCATION_URL,
HAS_TOKEN_URL,
>;

fn default_token_type() -> Option<BasicTokenType> {
Expand Down Expand Up @@ -151,17 +162,15 @@ fn main() {
.expect("Invalid token endpoint URL");

// Set up the config for the Wunderlist OAuth2 process.
let client = SpecialClient::new(
wunder_client_id,
Some(wunderlist_client_secret),
auth_url,
Some(token_url),
)
// This example will be running its own server at localhost:8080.
// See below for the server implementation.
.set_redirect_uri(
RedirectUrl::new("http://localhost:8080".to_string()).expect("Invalid redirect URL"),
);
let client = SpecialClient::new(wunder_client_id)
.set_client_secret(wunderlist_client_secret)
.set_auth_url(auth_url)
.set_token_url(token_url)
// This example will be running its own server at localhost:8080.
// See below for the server implementation.
.set_redirect_uri(
RedirectUrl::new("http://localhost:8080".to_string()).expect("Invalid redirect URL"),
);

// Generate the authorization URL to which we'll redirect the user.
let (authorize_url, csrf_state) = client.authorize_url(CsrfToken::new_random).url();
Expand Down
13 changes: 12 additions & 1 deletion src/basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,24 @@ use crate::{
///
/// Basic OAuth2 client specialization, suitable for most applications.
///
pub type BasicClient = Client<
pub type BasicClient<
const HAS_AUTH_URL: bool = false,
const HAS_DEVICE_AUTH_URL: bool = false,
const HAS_INTROSPECTION_URL: bool = false,
const HAS_REVOCATION_URL: bool = false,
const HAS_TOKEN_URL: bool = false,
> = Client<
BasicErrorResponse,
BasicTokenResponse,
BasicTokenType,
BasicTokenIntrospectionResponse,
StandardRevocableToken,
BasicRevocationErrorResponse,
HAS_AUTH_URL,
HAS_DEVICE_AUTH_URL,
HAS_INTROSPECTION_URL,
HAS_REVOCATION_URL,
HAS_TOKEN_URL,
>;

///
Expand Down
Loading

0 comments on commit 1d1f4d1

Please sign in to comment.