Skip to main content

ExternalAuth

Trait ExternalAuth 

Source
pub trait ExternalAuth:
    Send
    + Sync
    + 'static {
    type User: PanelUser;

    // Required methods
    fn current_user(
        &self,
        cx: &Cx,
    ) -> impl Future<Output = Result<Option<Self::User>>> + Send;
    fn login_url(&self, cx: &Cx, next: &str) -> String;
    fn logout_url(&self, cx: &Cx) -> String;
}
Expand description

A host app’s own sign-in, which a panel trusts in place of a login form (ADR-0013).

Auth::external installs it. The app keeps its login page, its sessions and its credentials; a panel user is whoever current_user says the request’s app session belongs to.

An anonymous request to the panel’s {prefix}/login goes to login_url. Once the app has signed the user in and sent them back, the panel starts a panel session of its own, which holds what the panel keeps per browser, such as the selected tenant. On every later request the gate asks current_user again and serves a session only while it names the session’s user: signing out of the app, or into another account, takes effect on the panel’s next request.

Each panel keeps its session in a cookie of its own, so entering this panel leaves a session held on another as it was.

use tablo_core::{ExternalAuth, PanelUser, auth::safe_next};
use topcoat::{context::Cx, session};

struct Staff {
    id: String,
    name: String,
    admin: bool,
}

impl PanelUser for Staff {
    fn user_id(&self) -> String {
        self.id.clone()
    }

    fn display_name(&self) -> &str {
        &self.name
    }

    fn can_access_panel(&self, _panel: &str) -> bool {
        self.admin
    }
}

async fn staff_for(_cx: &Cx, _hash: &session::TokenHash) -> topcoat::Result<Option<Staff>> {
    Ok(None) // the app's own session lookup
}

struct AppSession;

impl ExternalAuth for AppSession {
    type User = Staff;

    async fn current_user(&self, cx: &Cx) -> topcoat::Result<Option<Staff>> {
        match session::token_hash(cx).await? {
            Some(hash) => staff_for(cx, &hash).await,
            None => Ok(None),
        }
    }

    fn login_url(&self, _cx: &Cx, next: &str) -> String {
        format!("/login?return_to={next}")
    }

    fn logout_url(&self, _cx: &Cx) -> String {
        "/sign-out".to_owned()
    }
}

Required Associated Types§

Source

type User: PanelUser

The app’s user type.

Required Methods§

Source

fn current_user( &self, cx: &Cx, ) -> impl Future<Output = Result<Option<Self::User>>> + Send

The user the request’s app session signs in; None when it signs in nobody.

It runs on the gate of every panel request, and again when a live connection checks its session, long after the response started: it reads the request and never writes a cookie, so a sliding app session refreshes elsewhere. A live connection keeps the cookies of its handshake, so to end a session at once it must consult state the server holds: a signed cookie alone keeps naming its user until it expires. The app’s session cookie must also reach a page opened from another site, as SameSite=Lax does, or a followed link finds no user. A failure to reach the app’s store returns the error, which the panel logs and answers with an opaque 500 rather than as a signed-out user.

Source

fn login_url(&self, cx: &Cx, next: &str) -> String

Where a request with no app session signs in. next is the URL to come back to, percent-encoded to sit in a query string. Check it with safe_next before redirecting to it: the browser can send anything.

Source

fn logout_url(&self, cx: &Cx) -> String

Where the shell’s Sign out goes, once it has ended the panel’s session. That ends only the panel’s, so while the app’s lives the user’s next panel request signs them in again. Name a route that ends the app’s session, or a page of the app’s that offers to: the browser reaches it by GET, and it may lie on another origin, such as an SSO provider’s.

Dyn Compatibility§

This trait is not dyn compatible.

In older versions of Rust, dyn compatibility was called "object safety".

Implementors§