about summary refs log tree commit diff
path: root/indieauth
diff options
context:
space:
mode:
Diffstat (limited to 'indieauth')
-rw-r--r--indieauth/Cargo.toml2
-rw-r--r--indieauth/src/lib.rs117
2 files changed, 111 insertions, 8 deletions
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",
         }
     }
 }