diff options
-rw-r--r-- | Cargo.lock | 2 | ||||
-rw-r--r-- | Cargo.toml | 2 | ||||
-rw-r--r-- | indieauth/Cargo.toml | 2 | ||||
-rw-r--r-- | indieauth/src/lib.rs | 117 | ||||
-rw-r--r-- | src/indieauth/mod.rs | 6 | ||||
-rw-r--r-- | templates-neo/Cargo.toml | 2 | ||||
-rw-r--r-- | templates/Cargo.toml | 2 |
7 files changed, 119 insertions, 14 deletions
diff --git a/Cargo.lock b/Cargo.lock index b0cf21b..226a14d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1710,7 +1710,7 @@ dependencies = [ [[package]] name = "kittybox-indieauth" -version = "0.1.0" +version = "0.2.0" dependencies = [ "axum-core", "data-encoding", diff --git a/Cargo.toml b/Cargo.toml index af034c6..c38dabd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,7 +63,7 @@ features = ["fs"] version = "0.1.0" path = "./templates" [dependencies.kittybox-indieauth] -version = "0.1.0" +version = "0.2.0" path = "./indieauth" features = ["axum"] diff --git a/indieauth/Cargo.toml b/indieauth/Cargo.toml index d6bc1fe..8d2dc90 100644 --- a/indieauth/Cargo.toml +++ b/indieauth/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kittybox-indieauth" -version = "0.1.0" +version = "0.2.0" edition = "2021" [features] diff --git a/indieauth/src/lib.rs b/indieauth/src/lib.rs index 9841b53..05122b0 100644 --- a/indieauth/src/lib.rs +++ b/indieauth/src/lib.rs @@ -68,7 +68,14 @@ pub enum RevocationEndpointAuthMethod { pub enum ResponseType { /// An authorization code will be issued if this response type is /// requested. - Code + Code, + /// A token for an external realm will be issued if this response + /// type is requested. See [AutoAuth spec] for more details. + /// + /// This response type requires a valid access token. + /// + /// [AutoAuth spec]: https://github.com/sknebel/AutoAuth/blob/master/AutoAuth.md#allowing-external-clients-to-obtain-tokens + ExternalToken } // TODO serde_variant impl ResponseType { @@ -76,6 +83,7 @@ impl ResponseType { pub fn as_str(&self) -> &'static str { match self { ResponseType::Code => "code", + ResponseType::ExternalToken => "external_token", } } } @@ -325,7 +333,56 @@ pub struct AuthorizationRequest { /// indicate which profile URL the client is expecting in the /// resulting profile URL response or access token response. #[serde(skip_serializing_if = "Option::is_none")] - pub me: Option<Url> + pub me: Option<Url>, +} + +/// An authorization request that must be POSTed to an IndieAuth +/// endpoint together with a token with a scope of +/// `request_external_token:<scope>` to request external tokens with +/// the specific scope. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AutoAuthRequest { + /// The response type expected for this request. + /// Always [`ResponseType::ExternalToken`]. + pub response_type: ResponseType, + /// URL for which the external token must be obtained. + pub target_url: Url, + /// An array of scopes that are requested for a token. All scopes + /// must have a matching scope in the `request_external_token` + /// realm. + pub scope: Scopes, + /// AutoAuth callback data. If not specified, polling will be + /// used to return a token. + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(flatten)] + pub callback: Option<AutoAuthCallbackData>, +} + +/// Data to be used to establish an AutoAuth callback. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AutoAuthCallbackData { + state: State, + callback_url: Url +} + +#[inline(always)] +fn deserialize_secs<'de, D: serde::de::Deserializer<'de>>(d: D) -> Result<std::time::Duration, D::Error> { + use serde::Deserialize; + Ok(std::time::Duration::from_secs(u64::deserialize(d)?)) +} + +#[inline(always)] +fn serialize_secs<S: serde::ser::Serializer>(d: &std::time::Duration, s: S) -> Result<S::Ok, S::Error> { + s.serialize_u64(std::time::Duration::as_secs(d)) +} + +/// Response to be returned in the start of AutoAuth polling flow. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AutoAuthPollingResponse { + request_id: State, + #[serde(serialize_with = "serialize_secs")] + #[serde(deserialize_with = "deserialize_secs")] + interval: std::time::Duration } /// The authorization response that must be appended to the @@ -360,6 +417,32 @@ pub struct AuthorizationResponse { pub iss: Url } + +/// A special grant request that is used in the AutoAuth ceremony. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct AutoAuthCodeGrant { + /// Code that the requester's authorization endpoint generated. + code: String, + /// Client ID that this grant belongs to. Must always be the + /// requester's authorization endpoint. + client_id: Url, + /// Root URI of the protection space. + root_uri: Url, + /// The authorization realm requested if any. + realm: Option<String>, + /// Scopes that the authorization endpoint trusts the client with. + scope: Scopes, + /// Randomly chosen spoofing-protection state. + /// + /// **DO NOT REUSE CLIENT STATE.** + state: State, + /// Callback URL to send the token to once the ceremony is done. + callback_url: Url, + /// The user's URL. Will be used to confirm the authorization + /// endpoint's authority. + me: Url +} + /// A grant request that continues the IndieAuth ceremony. #[derive(PartialEq, Debug, Clone, Serialize, Deserialize)] #[serde(tag = "grant_type")] @@ -416,6 +499,9 @@ pub enum GrantResponse { /// An access token response, containing an access token, a refresh /// token (if the identity provider supports them) and the profile /// (if access was granted to the profile data). + /// + /// Is also used for AutoAuth: leave `profile` empty, do not issue + /// a refresh token, pass `state`. AccessToken { /// The URL for the user this token corresponds to. me: Url, @@ -426,15 +512,18 @@ pub enum GrantResponse { /// requested. Absence from IndieAuth spec confirmed as /// erroneous. scope: Option<Scopes>, - /// The user's profile information, if it was requested. - #[serde(skip_serializing_if = "Option::is_none")] - profile: Option<Profile>, /// The access token that can be used to access protected resources. access_token: String, + /// Only used in AutoAuth. Protects from spoofing. + #[serde(skip_serializing_if = "Option::is_none")] + state: Option<State>, /// The duration in which the access token expires, represented in seconds. // TODO replace with std::time::Duration #[serde(skip_serializing_if = "Option::is_none")] expires_in: Option<u64>, + /// The user's profile information, if it was requested. + #[serde(skip_serializing_if = "Option::is_none")] + profile: Option<Profile>, /// The refresh token, if it was issued. #[serde(skip_serializing_if = "Option::is_none")] refresh_token: Option<String> @@ -476,7 +565,8 @@ impl axum_core::response::IntoResponse for GrantResponse { #[allow(missing_docs)] pub enum RequestMaybeAuthorizationEndpoint { Authorization(AuthorizationRequest), - Grant(GrantRequest) + Grant(GrantRequest), + AutoAuth(AutoAuthCodeGrant) } /// A token introspection request that can be handled by the token @@ -669,7 +759,17 @@ pub enum ErrorKind { UnsupportedGrantType, /// The requested scope is invalid, unknown, malformed, or /// exceeds the scope granted by the resource owner. - InvalidScope + InvalidScope, + /// AutoAuth/OAuth2 Device Flow: authorization ceremony is still + /// being performed. Wait `interval` seconds and try again. + AuthorizationPending, + /// AutoAuth/OAuth2 Device Flow: You're polling too fast, slow + /// down by 5 seconds for all subsequent requests. + SlowDown, + /// AutoAuth/OAuth2 Device Flow: Access was denied by the + /// authorization endpoint. + AccessDenied, + } // TODO consider relying on serde_variant for these conversions impl AsRef<str> for ErrorKind { @@ -681,6 +781,9 @@ impl AsRef<str> for ErrorKind { ErrorKind::UnauthorizedClient => "unauthorized_client", ErrorKind::UnsupportedGrantType => "unsupported_grant_type", ErrorKind::InvalidScope => "invalid_scope", + ErrorKind::AuthorizationPending => "authorization_pending", + ErrorKind::SlowDown => "slow_down", + ErrorKind::AccessDenied => "access_denied", } } } diff --git a/src/indieauth/mod.rs b/src/indieauth/mod.rs index def9dfc..811bec6 100644 --- a/src/indieauth/mod.rs +++ b/src/indieauth/mod.rs @@ -559,7 +559,8 @@ async fn token_endpoint_post<A: AuthBackend, D: Storage + 'static>( token_type: kittybox_indieauth::TokenType::Bearer, scope: Some(scope), expires_in: Some(ACCESS_TOKEN_VALIDITY), - refresh_token: Some(refresh_token) + refresh_token: Some(refresh_token), + state: None }.into_response() }, GrantRequest::RefreshToken { @@ -653,7 +654,8 @@ async fn token_endpoint_post<A: AuthBackend, D: Storage + 'static>( token_type: kittybox_indieauth::TokenType::Bearer, scope: Some(scope), expires_in: Some(ACCESS_TOKEN_VALIDITY), - refresh_token: Some(refresh_token) + refresh_token: Some(refresh_token), + state: None }.into_response() } } diff --git a/templates-neo/Cargo.toml b/templates-neo/Cargo.toml index 09abbfb..98e70a7 100644 --- a/templates-neo/Cargo.toml +++ b/templates-neo/Cargo.toml @@ -32,7 +32,7 @@ features = ["serde"] version = "0.1.0" path = "../util" [dependencies.kittybox-indieauth] -version = "0.1.0" +version = "0.2.0" path = "../indieauth" [dependencies.microformats] version="^0.3.0" \ No newline at end of file diff --git a/templates/Cargo.toml b/templates/Cargo.toml index 38e73b3..9be5f30 100644 --- a/templates/Cargo.toml +++ b/templates/Cargo.toml @@ -29,5 +29,5 @@ features = ["serde"] version = "0.1.0" path = "../util" [dependencies.kittybox-indieauth] -version = "0.1.0" +version = "0.2.0" path = "../indieauth" \ No newline at end of file |