Skip to content

Commit

Permalink
Add conditional typestates
Browse files Browse the repository at this point in the history
1d1f4d1 added Boolean typestates to statically track, at compile time,
whether each endpoint has been set. In order to support scenarios such
as OpenID Connect Discovery in which it's not known until runtime
whether an Authorization Server has a particular endpoint, this change
introduces type-based typestates instead of using Boolean const
generics.

There are now three possible typestates, each implementing the
`EndpointState` trait:
 * `EndpointNotSet`: the corresponding endpoint has not been set and
   cannot be used.
 * `EndpointSet`: the corresponding endpoint has been set and is
   ready to be used.
 * `EndpointMaybeSet`: the corresponding endpoint may have been set and
   can be used via fallible methods that return
   `Result<_, ConfigurationError>`.

This change also adds conditional setters (e.g.,
`Client::set_auth_uri_option()`) to set the `EndpointMaybeSet`
typestate.

BREAKING CHANGE: The Boolean typestate parameters introduced in
5.0.0-alpha.1 have been replaced with the types listed above.
  • Loading branch information
ramosbugs committed Mar 2, 2024
1 parent d5d33f2 commit 85ea470
Show file tree
Hide file tree
Showing 13 changed files with 1,325 additions and 407 deletions.
61 changes: 38 additions & 23 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ with Rust releases older than 6 months will no longer be considered SemVer break
not result in a new major version number for this crate. MSRV changes will coincide with minor
version updates and will not happen in patch releases.

### Add typestate const generics to `Client`
### Add typestate generic types to `Client`

Each auth flow depends on one or more server endpoints. For example, the
authorization code flow depends on both an authorization endpoint and a token endpoint, while the
Expand All @@ -29,28 +29,43 @@ time, which endpoints' setters (e.g., `set_auth_uri()`) have been called. Auth f
an endpoint cannot be used without first calling the corresponding setter, which is enforced by the
compiler's type checker. This guarantees that certain errors will not arise at runtime.

In addition to unconditional setters (e.g., `set_auth_uri()`), each
endpoint has a corresponding conditional setter (e.g., `set_auth_uri_option()`) that sets a
conditional typestate (`EndpointMaybeSet`). When the conditional typestate is set, endpoints can
be used via fallible methods that return `Err(ConfigurationError::MissingUrl(_))` if an endpoint
has not been set. This is useful in dynamic scenarios such as
[OpenID Connect Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html), in which
it cannot be determined until runtime whether an endpoint is configured.

There are three possible typestates, each implementing the `EndpointState` trait:
* `EndpointNotSet`: the corresponding endpoint has **not** been set and cannot be used.
* `EndpointSet`: the corresponding endpoint **has** been set and is ready to be used.
* `EndpointMaybeSet`: the corresponding endpoint **may have** been set and can be used via fallible
methods that return `Result<_, ConfigurationError>`.

The following code changes are required to support the new interface:
1. Update calls to
[`Client::new()`](https://docs.rs/oauth2/latest/oauth2/struct.Client.html#method.new) to use the
single-argument constructor (which accepts only a `ClientId`). Use the `set_auth_uri()`,
`set_token_uri()`, and `set_client_secret()` methods to set the optional authorization endpoint,
`set_token_uri()`, and `set_client_secret()` methods to set the authorization endpoint,
token endpoint, and client secret, respectively, if applicable to your application's auth flows.
2. If required by your usage of the `Client` or `BasicClient` types (i.e., if you see related
compiler errors), add the following generic parameters:
```rust
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,
HasAuthUrl: EndpointState,
HasDeviceAuthUrl: EndpointState,
HasIntrospectionUrl: EndpointState,
HasRevocationUrl: EndpointState,
HasTokenUrl: EndpointState,
```
For example, if you store a `BasicClient` within another data type, you may need to annotate it
as `BasicClient<true, false, false, false, true>` if it has both an authorization endpoint and a
as `BasicClient<EndpointSet, EndpointNotSet, EndpointNotSet, EndpointNotSet, EndpointSet>` if it
has both an authorization endpoint and a
token endpoint set. Compiler error messages will likely guide you to the appropriate combination
of Boolean values.
of typestates.

If, instead of using `BasicClient`, you are directly using `Client` with a different set of type
parameters, you will need to append the five Boolean typestate parameters. For example, replace:
parameters, you will need to append the five generic typestate parameters. For example, replace:
```rust
type SpecialClient = Client<
BasicErrorResponse,
Expand All @@ -64,28 +79,28 @@ The following code changes are required to support the new interface:
with:
```rust
type SpecialClient<
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,
HasAuthUrl = EndpointNotSet,
HasDeviceAuthUrl = EndpointNotSet,
HasIntrospectionUrl = EndpointNotSet,
HasRevocationUrl = EndpointNotSet,
HasTokenUrl = EndpointNotSet,
> = Client<
BasicErrorResponse,
SpecialTokenResponse,
BasicTokenType,
BasicTokenIntrospectionResponse,
StandardRevocableToken,
BasicRevocationErrorResponse,
HAS_AUTH_URL,
HAS_DEVICE_AUTH_URL,
HAS_INTROSPECTION_URL,
HAS_REVOCATION_URL,
HAS_TOKEN_URL,
HasAuthUrl,
HasDeviceAuthUrl,
HasIntrospectionUrl,
HasRevocationUrl,
HasTokenUrl,
>;
```
The default values (`= false`) are optional but often helpful since they will allow you to
instantiate a client using `SpecialClient::new()` instead of having to specify
`SpecialClient::<false, false, false, false, false>::new()`.
The default values (`= EndpointNotSet`) are optional but often helpful since they will allow you
to instantiate a client using `SpecialClient::new()` instead of having to specify
`SpecialClient::<EndpointNotSet, EndpointNotSet, EndpointNotSet, EndpointNotSet, EndpointNotSet>::new()`.

### Rename endpoint getters and setters for consistency

Expand Down
29 changes: 14 additions & 15 deletions examples/wunderlist.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,11 @@ use oauth2::basic::{
BasicErrorResponse, BasicRevocationErrorResponse, BasicTokenIntrospectionResponse,
BasicTokenType,
};
use oauth2::helpers;
use oauth2::reqwest::reqwest;
use oauth2::{
AccessToken, AuthUrl, AuthorizationCode, Client, ClientId, ClientSecret, CsrfToken,
EmptyExtraTokenFields, ExtraTokenFields, RedirectUrl, RefreshToken, Scope, TokenResponse,
TokenUrl,
EmptyExtraTokenFields, EndpointNotSet, ExtraTokenFields, RedirectUrl, RefreshToken, Scope,
TokenResponse, TokenUrl,
};
use oauth2::{StandardRevocableToken, TokenType};
use serde::{Deserialize, Serialize};
Expand All @@ -36,23 +35,23 @@ use std::time::Duration;

type SpecialTokenResponse = NonStandardTokenResponse<EmptyExtraTokenFields>;
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,
HasAuthUrl = EndpointNotSet,
HasDeviceAuthUrl = EndpointNotSet,
HasIntrospectionUrl = EndpointNotSet,
HasRevocationUrl = EndpointNotSet,
HasTokenUrl = EndpointNotSet,
> = Client<
BasicErrorResponse,
SpecialTokenResponse,
BasicTokenType,
BasicTokenIntrospectionResponse,
StandardRevocableToken,
BasicRevocationErrorResponse,
HAS_AUTH_URL,
HAS_DEVICE_AUTH_URL,
HAS_INTROSPECTION_URL,
HAS_REVOCATION_URL,
HAS_TOKEN_URL,
HasAuthUrl,
HasDeviceAuthUrl,
HasIntrospectionUrl,
HasRevocationUrl,
HasTokenUrl,
>;

fn default_token_type() -> Option<BasicTokenType> {
Expand All @@ -78,8 +77,8 @@ pub struct NonStandardTokenResponse<EF: ExtraTokenFields> {
#[serde(skip_serializing_if = "Option::is_none")]
refresh_token: Option<RefreshToken>,
#[serde(rename = "scope")]
#[serde(deserialize_with = "helpers::deserialize_space_delimited_vec")]
#[serde(serialize_with = "helpers::serialize_space_delimited_vec")]
#[serde(deserialize_with = "oauth2::helpers::deserialize_space_delimited_vec")]
#[serde(serialize_with = "oauth2::helpers::serialize_space_delimited_vec")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
scopes: Option<Vec<Scope>>,
Expand Down
24 changes: 12 additions & 12 deletions src/basic.rs
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
use crate::{
revocation::{RevocationErrorResponseType, StandardRevocableToken},
Client, EmptyExtraTokenFields, ErrorResponseType, RequestTokenError, StandardErrorResponse,
StandardTokenIntrospectionResponse, StandardTokenResponse, TokenType,
Client, EmptyExtraTokenFields, EndpointNotSet, ErrorResponseType, RequestTokenError,
StandardErrorResponse, StandardTokenIntrospectionResponse, StandardTokenResponse, TokenType,
};

use std::fmt::Error as FormatterError;
use std::fmt::{Debug, Display, Formatter};

/// Basic OAuth2 client specialization, suitable for most applications.
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,
HasAuthUrl = EndpointNotSet,
HasDeviceAuthUrl = EndpointNotSet,
HasIntrospectionUrl = EndpointNotSet,
HasRevocationUrl = EndpointNotSet,
HasTokenUrl = EndpointNotSet,
> = Client<
BasicErrorResponse,
BasicTokenResponse,
BasicTokenType,
BasicTokenIntrospectionResponse,
StandardRevocableToken,
BasicRevocationErrorResponse,
HAS_AUTH_URL,
HAS_DEVICE_AUTH_URL,
HAS_INTROSPECTION_URL,
HAS_REVOCATION_URL,
HAS_TOKEN_URL,
HasAuthUrl,
HasDeviceAuthUrl,
HasIntrospectionUrl,
HasRevocationUrl,
HasTokenUrl,
>;

/// Basic OAuth2 authorization token types.
Expand Down
Loading

0 comments on commit 85ea470

Please sign in to comment.