about summary refs log blame commit diff
path: root/src/indieauth/mod.rs
blob: b3db77fe03aef5e654844702ce71bee635efbb1f (plain) (tree)
1
2
3
4
5
6
7
8
9
                             
                               
                       
           
                                                                                                                  
  
                                                             
                                                     
                                                                                                          
                             
                         
                                                                                                                                                                                                                                                                                                                                                          
  
                      
                
                            
             


                                                                              
                                                                  
 








































                                                                                        
                                                                                                                        
                                            
                                                                                                                    
                                               
                                                                                

                                                                
                                      
 
                                                             
                                                                  
 









                                                         
                      
               
                                                                         
 
                                                                            
              

















                                                                           
                                                                                                 


                                                                     
                                                    
     
 
                                                                          
                     
                                                
                        
                                                                 
                         
               









                                                                                                                              
                                                                                      
                                                                  


                                                             
                                                                                                                                                                                      
                                                          
                                                                             
                                                           
                                














                                                                                               
                                                    
                                   
                                                      



                                                                                    
                                     


















                                                                                                             
                                                                                               


                                                                                                                                                              

                                                                                       


                                                                             

                     











                                                                                        
                                                                                



                                                                    




                                                          
                                               
                                               
                      
                   
                                                                       

                                                                             
                      
                      
                  
                        
 


                             
                                







                                                      
                                              


                                           
                                                                   


                                                                                        
                                    







                                                             
                                                   
                                                        
                             
                       
                                                        
                                                                     
                                
                                                                 

                                     



                                                                
 









































                                                                                 
                                 
                                                                


                        
                                         
                                                                           
                     
                             
                                    
               
                                           












                                                                                         
                             
                                                                              
                                                                             
                 




























                                                                                                                
                                                   
                                                                  
                                                      






                                                      


                                                                            
                                                                                           
 
                                                                                
                     

                    
              
                                                                                 
          



                                                                                                           

     
                                         
                                                                   
                     
                             
                                    














                                                                                            
         


















                                                                                             
                                                                     
                 




                                         
                                                                                     
                                          

                                                                                         
                                  




                                                                              
                                                                              
                                                                           
                              

                                                                                                        
                                  
              












                                                                                                                
                                                              
                              

                                                                                                       
                                  
             









                                                                                                   



                                            
                                                 




                                                                                           






















                                                                                  
                                        

                             
                                                                  
                                                        
                                                   
                             
          




                                                                                   
                                         
                                          

                                                                              
                                  





                                                                               
                              

                                                                              
                                  
             
                                                        
                                  

                                                                                                                      
                                      




                                                                                                 
              
 
                                                         










                                                                                           














                                                                                       
                                                                                





                                                                             
                                                                                           

                                                                         
 
                                        

                             
                                                                  
                                                        
                                                   
                             


         
                                                    
                                                     
                     
                                                                               
                             
                                                         
               
                         

                                                                     
                                 
                                                            











                                                                                       
                                                                                                         





                                                                    
                            

                                                  
                     
                             
                                                   
                        
                                                                     
                                       
                                                            







                                                         
                                



                                               












                                                                                                    
                                                                          

                                                                                      
          



                                                                                                




                                                                     
                                                                               
                             
               
                         

                                                                     










                                                                                     
                                                                                      



















                                                                                       
 


                                             
                                                          
                                     

                                           

                                   
                                     
                            
                                                                   
                       

                                                              
                             
                                                      






                                                           
                                                       
 
                                                
                           
                                                                                                
                                                                                                                     
                        




                                                            
         
               
                                                      

                                                    

                                 
 




















                                                                                         
                                          



                                                                    
use std::marker::PhantomData;
use microformats::types::Class;
use tracing::error;
use serde::Deserialize;
use axum::{
    extract::{Form, FromRef, Host, Json, Query, State}, http::StatusCode, response::{Html, IntoResponse, Response}
};
#[cfg_attr(not(feature = "webauthn"), allow(unused_imports))]
use axum_extra::extract::cookie::{CookieJar, Cookie};
use axum_extra::{headers::{authorization::Bearer, Authorization, ContentType, HeaderMapExt}, TypedHeader};
use crate::database::Storage;
use kittybox_indieauth::{
    AuthorizationRequest, AuthorizationResponse, ClientMetadata, Error, ErrorKind, GrantRequest, GrantResponse, GrantType, IntrospectionEndpointAuthMethod, Metadata, PKCEMethod, Profile, ProfileUrl, ResponseType, RevocationEndpointAuthMethod, Scope, Scopes, TokenData, TokenIntrospectionRequest, TokenIntrospectionResponse, TokenRevocationRequest
};
use std::str::FromStr;

pub mod backend;
#[cfg(feature = "webauthn")]
mod webauthn;
use backend::AuthBackend;

const ACCESS_TOKEN_VALIDITY: u64 = 7 * 24 * 60 * 60; // 7 days
const REFRESH_TOKEN_VALIDITY: u64 = ACCESS_TOKEN_VALIDITY / 7 * 60; // 60 days
/// Internal scope for accessing the token introspection endpoint.
const KITTYBOX_TOKEN_STATUS: &str = "kittybox:token_status";

pub(crate) struct User<A: AuthBackend>(pub(crate) TokenData, pub(crate) PhantomData<A>);
impl<A: AuthBackend> std::fmt::Debug for User<A> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_tuple("User").field(&self.0).finish()
    }
}
impl<A: AuthBackend> std::ops::Deref for User<A> {
    type Target = TokenData;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

pub enum IndieAuthResourceError {
    InvalidRequest,
    Unauthorized,
    InvalidToken
}
impl axum::response::IntoResponse for IndieAuthResourceError {
    fn into_response(self) -> axum::response::Response {
        use IndieAuthResourceError::*;

        match self {
            Unauthorized => (
                StatusCode::UNAUTHORIZED,
                [("WWW-Authenticate", "Bearer")]
            ).into_response(),
            InvalidRequest => (
                StatusCode::BAD_REQUEST,
                Json(&serde_json::json!({"error": "invalid_request"}))
            ).into_response(),
            InvalidToken => (
                StatusCode::UNAUTHORIZED,
                [("WWW-Authenticate", "Bearer, error=\"invalid_token\"")],
                Json(&serde_json::json!({"error": "unauthorized"}))
            ).into_response()
        }
    }
}

#[async_trait::async_trait]
impl <A: AuthBackend + FromRef<St>, St: Clone + Send + Sync + 'static> axum::extract::FromRequestParts<St> for User<A> {
    type Rejection = IndieAuthResourceError;

    async fn from_request_parts(req: &mut axum::http::request::Parts, state: &St) -> Result<Self, Self::Rejection> {
        let TypedHeader(Authorization(token)) =
            TypedHeader::<Authorization<Bearer>>::from_request_parts(req, state)
            .await
            .map_err(|_| IndieAuthResourceError::Unauthorized)?;

        let auth = A::from_ref(state);

        let Host(host) = Host::from_request_parts(req, state)
            .await
            .map_err(|_| IndieAuthResourceError::InvalidRequest)?;

        auth.get_token(
            &format!("https://{host}/").parse().unwrap(),
            token.token()
        )
            .await
            .unwrap()
            .ok_or(IndieAuthResourceError::InvalidToken)
            .map(|t| User(t, PhantomData))
    }
}

pub async fn metadata(
    Host(host): Host
) -> Metadata {
    let issuer: url::Url = format!("https://{}/", host).parse().unwrap();

    let indieauth: url::Url = issuer.join("/.kittybox/indieauth/").unwrap();
    Metadata {
        issuer,
        authorization_endpoint: indieauth.join("auth").unwrap(),
        token_endpoint: indieauth.join("token").unwrap(),
        introspection_endpoint: indieauth.join("token_status").unwrap(),
        introspection_endpoint_auth_methods_supported: Some(vec![
            IntrospectionEndpointAuthMethod::Bearer
        ]),
        revocation_endpoint: Some(indieauth.join("revoke_token").unwrap()),
        revocation_endpoint_auth_methods_supported: Some(vec![
            RevocationEndpointAuthMethod::None
        ]),
        scopes_supported: Some(vec![
            Scope::Create,
            Scope::Update,
            Scope::Delete,
            Scope::Media,
            Scope::Profile
        ]),
        response_types_supported: Some(vec![ResponseType::Code]),
        grant_types_supported: Some(vec![GrantType::AuthorizationCode, GrantType::RefreshToken]),
        service_documentation: None,
        code_challenge_methods_supported: vec![PKCEMethod::S256],
        authorization_response_iss_parameter_supported: Some(true),
        userinfo_endpoint: Some(indieauth.join("userinfo").unwrap()),
        client_id_metadata_document_supported: true,
    }
}

async fn authorization_endpoint_get<A: AuthBackend, D: Storage + 'static>(
    Host(host): Host,
    Query(request): Query<AuthorizationRequest>,
    State(db): State<D>,
    State(http): State<reqwest_middleware::ClientWithMiddleware>,
    State(auth): State<A>
) -> Response {
    let me: url::Url = format!("https://{host}/").parse().unwrap();
    // XXX: attempt fetching OAuth application metadata
    let h_app: ClientMetadata = if request.client_id.domain().unwrap() == "localhost" && me.domain().unwrap() != "localhost" {
        // If client is localhost, but we aren't localhost, generate synthetic metadata.
        tracing::warn!("Client is localhost, not fetching metadata");
        let mut metadata = ClientMetadata::new(request.client_id.clone(), request.client_id.clone()).unwrap();

        metadata.client_name = Some("Your locally hosted app".to_string());

        metadata
    } else {
        tracing::debug!("Sending request to {} to fetch metadata", request.client_id);
        let metadata_request = http.get(request.client_id.clone())
            .header("Accept", "application/json, text/html");
        match metadata_request.send().await
            .and_then(|res| res.error_for_status()
                .map_err(reqwest_middleware::Error::Reqwest))
        {
            Ok(response) if response.headers().typed_get::<ContentType>().to_owned().map(mime::Mime::from).map(|m| m.type_() == "text" && m.subtype() == "html").unwrap_or(false) => {
                let url = response.url().clone();
                let text = response.text().await.unwrap();
                tracing::debug!("Received {} bytes in response", text.len());
                match microformats::from_html(&text, url) {
                    Ok(mf2) => {
                        if let Some(relation) = mf2.rels.items.get(&request.redirect_uri) {
                            if !relation.rels.iter().any(|i| i == "redirect_uri") {
                                return (StatusCode::BAD_REQUEST,
                                        [("Content-Type", "text/plain")],
                                        "The redirect_uri provided was declared as \
                                         something other than redirect_uri.")
                                    .into_response()
                            }
                        } else if request.redirect_uri.origin() != request.client_id.origin() {
                            return (StatusCode::BAD_REQUEST,
                                    [("Content-Type", "text/plain")],
                                    "The redirect_uri didn't match the origin \
                                     and wasn't explicitly allowed. You were being tricked.")
                                .into_response()
                        }

                        if let Some(app) = mf2.items
                            .iter()
                            .find(|&i| i.r#type.iter()
                                .any(|i| {
                                    *i == Class::from_str("h-app").unwrap()
                                        || *i == Class::from_str("h-x-app").unwrap()
                                })
                            )
                            .cloned()
                        {
                            // Create a synthetic metadata document. Be forgiving.
                            let mut metadata = ClientMetadata::new(
                                request.client_id.clone(),
                                app.properties.get("url")
                                    .and_then(|v| v.first())
                                    .and_then(|i| match i {
                                        microformats::types::PropertyValue::Url(url) => Some(url.clone()),
                                        _ => None
                                    })
                                    .unwrap_or_else(|| request.client_id.clone())
                            ).unwrap();

                            metadata.client_name = app.properties.get("name")
                                .and_then(|v| v.first())
                                .and_then(|i| match i {
                                    microformats::types::PropertyValue::Plain(name) => Some(name.to_owned()),
                                    _ => None
                                });

                            metadata.redirect_uris = mf2.rels.by_rels().remove("redirect_uri");

                            metadata
                        } else {
                            return (StatusCode::BAD_REQUEST, [("Content-Type", "text/plain")], "No h-app or JSON application metadata found.").into_response()
                        }
                    },
                    Err(err) => {
                        tracing::error!("Error parsing application metadata: {}", err);
                        return (
                            StatusCode::BAD_REQUEST,
                            [("Content-Type", "text/plain")],
                            "Parsing h-app metadata failed.").into_response()
                    }
                }
            },
            Ok(response) => match response.json::<ClientMetadata>().await {
                Ok(client_metadata) => {
                    client_metadata
                },
                Err(err) => {
                    tracing::error!("Error parsing JSON application metadata: {}", err);
                    return (
                        StatusCode::BAD_REQUEST,
                        [("Content-Type", "text/plain")],
                        format!("Parsing OAuth2 JSON app metadata failed: {}", err)
                    ).into_response()
                }
            },
            Err(err) => {
                tracing::error!("Error fetching application metadata: {}", err);
                return (
                    StatusCode::BAD_REQUEST,
                    [("Content-Type", "text/plain")],
                    format!("Fetching app metadata failed: {}", err)
                ).into_response()
            }
        }
    };

    tracing::debug!("Application metadata: {:#?}", h_app);

    Html(kittybox_frontend_renderer::Template {
        title: "Confirm sign-in via IndieAuth",
        blog_name: "Kittybox",
        feeds: vec![],
        user: None,
        content: kittybox_frontend_renderer::AuthorizationRequestPage {
            request,
            credentials: auth.list_user_credential_types(&me).await.unwrap(),
            user: db.get_post(me.as_str()).await.unwrap().unwrap(),
            app: h_app
        }.to_string(),
    }.to_string())
        .into_response()
}

#[derive(Deserialize, Debug)]
#[serde(untagged)]
enum Credential {
    Password(String),
    #[cfg(feature = "webauthn")]
    WebAuthn(::webauthn::prelude::PublicKeyCredential)
}

#[derive(Deserialize, Debug)]
struct AuthorizationConfirmation {
    authorization_method: Credential,
    request: AuthorizationRequest
}

#[tracing::instrument(skip(auth, credential))]
async fn verify_credential<A: AuthBackend>(
    auth: &A,
    website: &url::Url,
    credential: Credential,
    #[cfg_attr(not(feature = "webauthn"), allow(unused_variables))]
    challenge_id: Option<&str>
) -> std::io::Result<bool> {
    match credential {
        Credential::Password(password) => auth.verify_password(website, password).await,
        #[cfg(feature = "webauthn")]
        Credential::WebAuthn(credential) => webauthn::verify(
            auth,
            website,
            credential,
            challenge_id.unwrap()
        ).await
    }
}

#[tracing::instrument(skip(backend, confirmation))]
async fn authorization_endpoint_confirm<A: AuthBackend>(
    Host(host): Host,
    State(backend): State<A>,
    cookies: CookieJar,
    Json(confirmation): Json<AuthorizationConfirmation>,
) -> Response {
    tracing::debug!("Received authorization confirmation from user");
    #[cfg(feature = "webauthn")]
    let challenge_id = cookies.get(webauthn::CHALLENGE_ID_COOKIE)
        .map(|cookie| cookie.value());
    #[cfg(not(feature = "webauthn"))]
    let challenge_id = None;

    let website = format!("https://{}/", host).parse().unwrap();
    let AuthorizationConfirmation {
        authorization_method: credential,
        request: mut auth
    } = confirmation;

    match verify_credential(&backend, &website, credential, challenge_id).await {
        Ok(verified) => if !verified {
            error!("User failed verification, bailing out.");
            return StatusCode::UNAUTHORIZED.into_response();
        },
        Err(err) => {
            error!("Error while verifying credential: {}", err);
            return StatusCode::INTERNAL_SERVER_ERROR.into_response();
        }
    }
    // Insert the correct `me` value into the request
    //
    // From this point, the `me` value that hits the backend is
    // guaranteed to be authoritative and correct, and can be safely
    // unwrapped.
    auth.me = Some(website.clone());
    // Cloning these two values, because we can't destructure
    // the AuthorizationRequest - we need it for the code
    let state = auth.state.clone();
    let redirect_uri = auth.redirect_uri.clone();

    let code = match backend.create_code(auth).await {
        Ok(code) => code,
        Err(err) => {
            error!("Error creating authorization code: {}", err);
            return StatusCode::INTERNAL_SERVER_ERROR.into_response();
        }
    };

    let location = {
        let mut uri = redirect_uri;
        uri.set_query(Some(&serde_urlencoded::to_string(
            AuthorizationResponse { code, state, iss: website }
        ).unwrap()));

        uri
    };

    // DO NOT SET `StatusCode::FOUND` here! `fetch()` cannot read from
    // redirects, it can only follow them or choose to receive an
    // opaque response instead that is completely useless
    (StatusCode::NO_CONTENT,
     [("Location", location.as_str())],
     #[cfg(feature = "webauthn")]
     cookies.remove(Cookie::from(webauthn::CHALLENGE_ID_COOKIE))
    )
        .into_response()
}

#[tracing::instrument(skip(backend, db))]
async fn authorization_endpoint_post<A: AuthBackend, D: Storage + 'static>(
    Host(host): Host,
    State(backend): State<A>,
    State(db): State<D>,
    Form(grant): Form<GrantRequest>,
) -> Response {
    tracing::debug!("Processing grant...");
    match grant {
        GrantRequest::AuthorizationCode {
            code,
            client_id,
            redirect_uri,
            code_verifier
        } => {
            let request: AuthorizationRequest = match backend.get_code(&code).await {
                Ok(Some(request)) => request,
                Ok(None) => return Error {
                    kind: ErrorKind::InvalidGrant,
                    msg: Some("The provided authorization code is invalid.".to_string()),
                    error_uri: None
                }.into_response(),
                Err(err) => {
                    tracing::error!("Error retrieving auth request: {}", err);
                    return StatusCode::INTERNAL_SERVER_ERROR.into_response();
                }
            };
            if client_id != request.client_id {
                return Error {
                    kind: ErrorKind::InvalidGrant,
                    msg: Some("This authorization code isn't yours.".to_string()),
                    error_uri: None
                }.into_response()
            }
            if redirect_uri != request.redirect_uri {
                return Error {
                    kind: ErrorKind::InvalidGrant,
                    msg: Some("This redirect_uri doesn't match the one the code has been sent to.".to_string()),
                    error_uri: None
                }.into_response()
            }
            if !request.code_challenge.verify(code_verifier) {
                return Error {
                    kind: ErrorKind::InvalidGrant,
                    msg: Some("The PKCE challenge failed.".to_string()),
                    // are RFCs considered human-readable? 😝
                    error_uri: "https://datatracker.ietf.org/doc/html/rfc7636#section-4.6".parse().ok()
                }.into_response()
            }
            let me: url::Url = format!("https://{}/", host).parse().unwrap();
            if request.me.unwrap() != me {
                return Error {
                    kind: ErrorKind::InvalidGrant,
                    msg: Some("This authorization endpoint does not serve this user.".to_string()),
                    error_uri: None
                }.into_response()
            }
            let profile = if request.scope.as_ref()
                                  .map(|s| s.has(&Scope::Profile))
                                  .unwrap_or_default()
            {
                match get_profile(
                    db,
                    me.as_str(),
                    request.scope.as_ref()
                        .map(|s| s.has(&Scope::Email))
                        .unwrap_or_default()
                ).await {
                    Ok(profile) => {
                        tracing::debug!("Retrieved profile: {:?}", profile);
                        profile
                    },
                    Err(err) => {
                        tracing::error!("Error retrieving profile from database: {}", err);

                        return StatusCode::INTERNAL_SERVER_ERROR.into_response()
                    }
                }
            } else {
                None
            };

            GrantResponse::ProfileUrl(ProfileUrl { me, profile }).into_response()
        },
        _ => Error {
            kind: ErrorKind::InvalidGrant,
            msg: Some("The provided grant_type is unusable on this endpoint.".to_string()),
            error_uri: "https://indieauth.spec.indieweb.org/#redeeming-the-authorization-code".parse().ok()
        }.into_response()
    }
}

#[tracing::instrument(skip(backend, db))]
async fn token_endpoint_post<A: AuthBackend, D: Storage + 'static>(
    Host(host): Host,
    State(backend): State<A>,
    State(db): State<D>,
    Form(grant): Form<GrantRequest>,
) -> Response {
    #[inline]
    fn prepare_access_token(me: url::Url, client_id: url::Url, scope: Scopes) -> TokenData {
        TokenData {
            me, client_id, scope,
            exp: (std::time::SystemTime::now()
                  .duration_since(std::time::UNIX_EPOCH)
                  .unwrap()
                  + std::time::Duration::from_secs(ACCESS_TOKEN_VALIDITY))
                .as_secs()
                .into(),
            iat: std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap()
                .as_secs()
                .into()
        }
    }

    #[inline]
    fn prepare_refresh_token(me: url::Url, client_id: url::Url, scope: Scopes) -> TokenData {
        TokenData {
            me, client_id, scope,
            exp: (std::time::SystemTime::now()
                  .duration_since(std::time::UNIX_EPOCH)
                  .unwrap()
                  + std::time::Duration::from_secs(REFRESH_TOKEN_VALIDITY))
                .as_secs()
                .into(),
            iat: std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap()
                .as_secs()
                .into()
        }
    }

    let me: url::Url = format!("https://{}/", host).parse().unwrap();

    match grant {
        GrantRequest::AuthorizationCode {
            code,
            client_id,
            redirect_uri,
            code_verifier
        } => {
            let request: AuthorizationRequest = match backend.get_code(&code).await {
                Ok(Some(request)) => request,
                Ok(None) => return Error {
                    kind: ErrorKind::InvalidGrant,
                    msg: Some("The provided authorization code is invalid.".to_string()),
                    error_uri: None
                }.into_response(),
                Err(err) => {
                    tracing::error!("Error retrieving auth request: {}", err);
                    return StatusCode::INTERNAL_SERVER_ERROR.into_response();
                }
            };

            tracing::debug!("Retrieved authorization request: {:?}", request);

            let scope = if let Some(scope) = request.scope { scope } else {
                return Error {
                    kind: ErrorKind::InvalidScope,
                    msg: Some("Tokens cannot be issued if no scopes are requested.".to_string()),
                    error_uri: "https://indieauth.spec.indieweb.org/#access-token-response".parse().ok()
                }.into_response();
            };
            if client_id != request.client_id {
                return Error {
                    kind: ErrorKind::InvalidGrant,
                    msg: Some("This authorization code isn't yours.".to_string()),
                    error_uri: None
                }.into_response()
            }
            if redirect_uri != request.redirect_uri {
                return Error {
                    kind: ErrorKind::InvalidGrant,
                    msg: Some("This redirect_uri doesn't match the one the code has been sent to.".to_string()),
                    error_uri: None
                }.into_response()
            }
            if !request.code_challenge.verify(code_verifier) {
                return Error {
                    kind: ErrorKind::InvalidGrant,
                    msg: Some("The PKCE challenge failed.".to_string()),
                    error_uri: "https://datatracker.ietf.org/doc/html/rfc7636#section-4.6".parse().ok()
                }.into_response();
            }

            // Note: we can trust the `request.me` value, since we set
            // it earlier before generating the authorization code
            if request.me.unwrap() != me {
                return Error {
                    kind: ErrorKind::InvalidGrant,
                    msg: Some("This authorization endpoint does not serve this user.".to_string()),
                    error_uri: None
                }.into_response()
            }

            let profile = if dbg!(scope.has(&Scope::Profile)) {
                match get_profile(
                    db,
                    me.as_str(),
                    scope.has(&Scope::Email)
                ).await {
                    Ok(profile) => dbg!(profile),
                    Err(err) => {
                        tracing::error!("Error retrieving profile from database: {}", err);

                        return StatusCode::INTERNAL_SERVER_ERROR.into_response()
                    }
                }
            } else {
                None
            };

            let access_token = match backend.create_token(
                prepare_access_token(me.clone(), client_id.clone(), scope.clone())
            ).await {
                Ok(token) => token,
                Err(err) => {
                    tracing::error!("Error creating access token: {}", err);
                    return StatusCode::INTERNAL_SERVER_ERROR.into_response();
                }
            };
            // TODO: only create refresh token if user allows it
            let refresh_token = match backend.create_refresh_token(
                prepare_refresh_token(me.clone(), client_id, scope.clone())
            ).await {
                Ok(token) => token,
                Err(err) => {
                    tracing::error!("Error creating refresh token: {}", err);
                    return StatusCode::INTERNAL_SERVER_ERROR.into_response();
                }
            };

            GrantResponse::AccessToken {
                me,
                profile,
                access_token,
                token_type: kittybox_indieauth::TokenType::Bearer,
                scope: Some(scope),
                expires_in: Some(ACCESS_TOKEN_VALIDITY),
                refresh_token: Some(refresh_token),
                state: None
            }.into_response()
        },
        GrantRequest::RefreshToken {
            refresh_token,
            client_id,
            scope
        } => {
            let data = match backend.get_refresh_token(&me, &refresh_token).await {
                Ok(Some(token)) => token,
                Ok(None) => return Error {
                    kind: ErrorKind::InvalidGrant,
                    msg: Some("This refresh token is not valid.".to_string()),
                    error_uri: None
                }.into_response(),
                Err(err) => {
                    tracing::error!("Error retrieving refresh token: {}", err);
                    return StatusCode::INTERNAL_SERVER_ERROR.into_response()
                }
            };

            if data.client_id != client_id {
                return Error {
                    kind: ErrorKind::InvalidGrant,
                    msg: Some("This refresh token is not yours.".to_string()),
                    error_uri: None
                }.into_response();
            }

            let scope = if let Some(scope) = scope {
                if !data.scope.has_all(scope.as_ref()) {
                    return Error {
                        kind: ErrorKind::InvalidScope,
                        msg: Some("You can't request additional scopes through the refresh token grant.".to_string()),
                        error_uri: None
                    }.into_response();
                }

                scope
            } else {
                // Note: check skipped because of redundancy (comparing a scope list with itself)
                data.scope
            };


            let profile = if scope.has(&Scope::Profile) {
                match get_profile(
                    db,
                    data.me.as_str(),
                    scope.has(&Scope::Email)
                ).await {
                    Ok(profile) => profile,
                    Err(err) => {
                        tracing::error!("Error retrieving profile from database: {}", err);

                        return StatusCode::INTERNAL_SERVER_ERROR.into_response()
                    }
                }
            } else {
                None
            };

            let access_token = match backend.create_token(
                prepare_access_token(data.me.clone(), client_id.clone(), scope.clone())
            ).await {
                Ok(token) => token,
                Err(err) => {
                    tracing::error!("Error creating access token: {}", err);
                    return StatusCode::INTERNAL_SERVER_ERROR.into_response();
                }
            };

            let old_refresh_token = refresh_token;
            let refresh_token = match backend.create_refresh_token(
                prepare_refresh_token(data.me.clone(), client_id, scope.clone())
            ).await {
                Ok(token) => token,
                Err(err) => {
                    tracing::error!("Error creating refresh token: {}", err);
                    return StatusCode::INTERNAL_SERVER_ERROR.into_response();
                }
            };
            if let Err(err) = backend.revoke_refresh_token(&me, &old_refresh_token).await {
                tracing::error!("Error revoking refresh token: {}", err);
                return StatusCode::INTERNAL_SERVER_ERROR.into_response();
            }

            GrantResponse::AccessToken {
                me: data.me,
                profile,
                access_token,
                token_type: kittybox_indieauth::TokenType::Bearer,
                scope: Some(scope),
                expires_in: Some(ACCESS_TOKEN_VALIDITY),
                refresh_token: Some(refresh_token),
                state: None
            }.into_response()
        }
    }
}

#[tracing::instrument(skip(backend, token_request))]
async fn introspection_endpoint_post<A: AuthBackend>(
    Host(host): Host,
    TypedHeader(Authorization(auth_token)): TypedHeader<Authorization<Bearer>>,
    State(backend): State<A>,
    Form(token_request): Form<TokenIntrospectionRequest>,
) -> Response {
    use serde_json::json;

    let me: url::Url = format!("https://{}/", host).parse().unwrap();

    // Check authentication first
    match backend.get_token(&me, auth_token.token()).await {
        Ok(Some(token)) => if !token.scope.has(&Scope::custom(KITTYBOX_TOKEN_STATUS)) {
            return (StatusCode::UNAUTHORIZED, Json(json!({
                "error": kittybox_indieauth::ResourceErrorKind::InsufficientScope
            }))).into_response();
        },
        Ok(None) => return (StatusCode::UNAUTHORIZED, Json(json!({
            "error": kittybox_indieauth::ResourceErrorKind::InvalidToken
        }))).into_response(),
        Err(err) => {
            tracing::error!("Error retrieving token data for introspection: {}", err);
            return StatusCode::INTERNAL_SERVER_ERROR.into_response()
        }
    }
    let response: TokenIntrospectionResponse = match backend.get_token(&me, &token_request.token).await {
        Ok(maybe_data) => maybe_data.into(),
        Err(err) => {
            tracing::error!("Error retrieving token data: {}", err);
            return StatusCode::INTERNAL_SERVER_ERROR.into_response()
        }
    };

    response.into_response()
}

async fn revocation_endpoint_post<A: AuthBackend>(
    Host(host): Host,
    State(backend): State<A>,
    Form(revocation): Form<TokenRevocationRequest>,
) -> impl IntoResponse {
    let me: url::Url = format!("https://{}/", host).parse().unwrap();

    if let Err(err) = tokio::try_join!(
        backend.revoke_token(&me, &revocation.token),
        backend.revoke_refresh_token(&me, &revocation.token)
    ) {
        tracing::error!("Error revoking token: {}", err);

        StatusCode::INTERNAL_SERVER_ERROR
    } else {
        StatusCode::OK
    }
}

#[tracing::instrument(skip(db))]
async fn get_profile<D: Storage + 'static>(
    db: D,
    url: &str,
    email: bool
) -> crate::database::Result<Option<Profile>> {
    fn get_first(v: serde_json::Value) -> Option<String> {
        match v {
            serde_json::Value::Array(mut a) => {
                a.truncate(1);
                match a.pop() {
                    Some(serde_json::Value::String(s)) => Some(s),
                    Some(serde_json::Value::Object(mut o)) => o.remove("value").and_then(get_first),
                    _ => None
                }
            },
            _ => None
        }
    }

    Ok(db.get_post(url).await?.map(|mut mf2| {
        // Ruthlessly manually destructure the MF2 document to save memory
        let mut properties = match mf2.as_object_mut().unwrap().remove("properties") {
            Some(serde_json::Value::Object(props)) => props,
            _ => unreachable!()
        };
        drop(mf2);
        let name = properties.remove("name").and_then(get_first);
        let url = properties.remove("uid").and_then(get_first).and_then(|u| u.parse().ok());
        let photo = properties.remove("photo").and_then(get_first).and_then(|u| u.parse().ok());
        let email = properties.remove("name").and_then(get_first);

        Profile { name, url, photo, email }
    }))
}

async fn userinfo_endpoint_get<A: AuthBackend, D: Storage + 'static>(
    Host(host): Host,
    TypedHeader(Authorization(auth_token)): TypedHeader<Authorization<Bearer>>,
    State(backend): State<A>,
    State(db): State<D>
) -> Response {
    use serde_json::json;

    let me: url::Url = format!("https://{}/", host).parse().unwrap();

    match backend.get_token(&me, auth_token.token()).await {
        Ok(Some(token)) => {
            if token.expired() {
                return (StatusCode::UNAUTHORIZED, Json(json!({
                    "error": kittybox_indieauth::ResourceErrorKind::InvalidToken
                }))).into_response();
            }
            if !token.scope.has(&Scope::Profile) {
                return (StatusCode::UNAUTHORIZED, Json(json!({
                    "error": kittybox_indieauth::ResourceErrorKind::InsufficientScope
                }))).into_response();
            }

            match get_profile(db, me.as_str(), token.scope.has(&Scope::Email)).await {
                Ok(Some(profile)) => profile.into_response(),
                Ok(None) => Json(json!({
                    // We do this because ResourceErrorKind is IndieAuth errors only
                    "error": "invalid_request"
                })).into_response(),
                Err(err) => {
                    tracing::error!("Error retrieving profile from database: {}", err);

                    StatusCode::INTERNAL_SERVER_ERROR.into_response()
                }
            }
        },
        Ok(None) => Json(json!({
            "error": kittybox_indieauth::ResourceErrorKind::InvalidToken
        })).into_response(),
        Err(err) => {
            tracing::error!("Error reading token: {}", err);

            StatusCode::INTERNAL_SERVER_ERROR.into_response()
        }
    }
}

pub fn router<St, A, S>() -> axum::Router<St>
where
    S: Storage + FromRef<St> + 'static,
    A: AuthBackend + FromRef<St>,
    reqwest_middleware::ClientWithMiddleware: FromRef<St>,
    St: Clone + Send + Sync + 'static
{
    use axum::routing::{Router, get, post};

    Router::new()
        .nest(
            "/.kittybox/indieauth",
            Router::new()
                .route("/metadata",
                       get(metadata))
                .route(
                    "/auth",
                    get(authorization_endpoint_get::<A, S>)
                        .post(authorization_endpoint_post::<A, S>))
                .route(
                    "/auth/confirm",
                    post(authorization_endpoint_confirm::<A>))
                .route(
                    "/token",
                    post(token_endpoint_post::<A, S>))
                .route(
                    "/token_status",
                    post(introspection_endpoint_post::<A>))
                .route(
                    "/revoke_token",
                    post(revocation_endpoint_post::<A>))
                .route(
                    "/userinfo",
                    get(userinfo_endpoint_get::<A, S>))

                .route("/webauthn/pre_register",
                       get(
                           #[cfg(feature = "webauthn")] webauthn::webauthn_pre_register::<A, S>,
                           #[cfg(not(feature = "webauthn"))] || std::future::ready(axum::http::StatusCode::NOT_FOUND)
                       )
                )
                .layer(tower_http::cors::CorsLayer::new()
                       .allow_methods([
                           axum::http::Method::GET,
                           axum::http::Method::POST
                       ])
                       .allow_origin(tower_http::cors::Any))
        )
        .route(
            "/.well-known/oauth-authorization-server",
            get(|| std::future::ready(
                (StatusCode::FOUND,
                 [("Location",
                   "/.kittybox/indieauth/metadata")]
                ).into_response()
            ))
        )
}

#[cfg(test)]
mod tests {
    #[test]
    fn test_deserialize_authorization_confirmation() {
        use super::{Credential, AuthorizationConfirmation};

        let confirmation = serde_json::from_str::<AuthorizationConfirmation>(r#"{
            "request":{
                "response_type": "code",
                "client_id": "https://quill.p3k.io/",
                "redirect_uri": "https://quill.p3k.io/",
                "state": "10101010",
                "code_challenge": "awooooooooooo",
                "code_challenge_method": "S256",
                "scope": "create+media"
            },
            "authorization_method": "swordfish"
        }"#).unwrap();

        match confirmation.authorization_method {
            Credential::Password(password) => assert_eq!(password.as_str(), "swordfish"),
            #[allow(unreachable_patterns)]
            other => panic!("Incorrect credential: {:?}", other)
        }
        assert_eq!(confirmation.request.state.as_ref(), "10101010");
    }
}