Something went wrong

Thank you for being patient! We're working hard on resolving the issue

Auth: AuthProvider + UserAuth - Lona Docs Log in

Auth: AuthProvider + UserAuth

The connector splits credential lookup (host-side) from credential shape (provider-side).

AuthProvider (provider-side)

A unit struct in your connector module that names the credential type:

pub struct MyProviderAuth;

impl AuthProvider for MyProviderAuth {
  const KIND: &'static str = "myprovider";
  type AuthId = str;
  type Credentials = Auth<MyProviderOAuthCredentials>;
}

Three pieces:

  • KIND — the stable string id under which the host stores this provider's credentials. Match-arm key for the host's UserAuth::sourced_credentials impl.
  • type AuthId — the per-account identifier type. Almost always str. Lookups receive &str (e.g. an authId derived from the rowkey segment).
  • type Credentials — the typed payload returned to the caller via UserAuthExt::auth::<MyProviderAuth>(...). Usually Auth<YourOAuthCredentials> from your OAuth helper.

UserAuth (host-side)

The connector defines this trait:

#[async_trait]
pub trait UserAuth: Send + Sync {
  async fn sourced_credentials(
    &self,
    kind: &'static str,
    aid: &str,
  ) -> anyhow::Result<Option<Box<dyn Any + Send + Sync>>>;
}

The host implements it once. The match-arm pattern:

async fn sourced_credentials(
  &self,
  kind: &'static str,
  aid: &str,
) -> anyhow::Result<Option<Box<dyn Any + Send + Sync>>> {
  match kind {
    "garmin"     => …box up Garmin creds…,
    "whoop"      => …box up Whoop creds…,
    "google"     => …box up Google creds…,
    "myprovider" => …box up MyProvider creds…,   // ← new arm
    _ => Err(anyhow!("unknown auth kind {kind}")),
  }
}

This is the only host-side change needed for credential lookup. Everything else flows through the typed auth::<P>(id) extension.

How a Row body uses it

fn client<'a>(
  user: &'a dyn UserAuth,
  _deps: &'a dyn MyProviderBackendDeps,
  p: &'a Self::Params,
) -> BoxFuture<'a, anyhow::Result<Self::Client>> {
  Box::pin(async move {
    let creds = user
      .auth::<MyProviderAuth>(&p.auth_id)
      .await
      .context("myprovider auth lookup")?
      .ok_or_else(|| anyhow!("no myprovider auth for {}", p.auth_id))?;
    // build your HTTP client from `creds`
    Ok(MyProviderClient { http: Arc::new(crate::Client::from(creds)) })
  })
}

The auth::<P>(id) blanket impl on UserAuth takes care of:

  1. Calling user.sourced_credentials(P::KIND, id).
  2. Downcasting the returned Box<dyn Any> to P::Credentials.
  3. Returning a typed Option<P::Credentials> to your row body.

If the credential type doesn't match — i.e. the host returned a different type than P::Credentialsauth::<P> errors with a clear message. So the host's match-arm bug shows up immediately at the call site.

RowKeyAuthProvider — for guards

Distinct from UserAuth: RowKeyAuthProvider is the host-side trait that answers "what email is this actor?" + "is this email an operator for scope X?". Used by guards (e.g. the weather guard that admits only operator-tester@…). Most providers don't interact with this directly.

OAuth lifecycle (host responsibility)

The connector's auth surface (AuthProvider + UserAuth) names the credential type and lookup but is intentionally silent on:

  • OAuth redirect flow — popup window, callback URL, exchange-code-for-token round-trip. The host owns this. Each in-tree provider (Google, Whoop, Garmin) currently implements its own browser flow; there is no helper layer yet (see "Gaps" in adding an integration).
  • Token storage — the host persists the typed MyProviderOAuthCredentials blob (typically encrypted JSON in lona_user_auth or equivalent) keyed by (user_id, kind, auth_id). UserAuth::sourced_credentials loads from that store at request time.
  • Refresh-token lifecycle — when an access token expires, the host re-runs the refresh exchange and updates the stored credentials. The connector never sees the refresh path; row bodies always receive a fresh Auth<...> handle from user.auth::<MyProviderAuth>(id). If the host's refresh fails, the lookup returns None and the row body surfaces a clear "no myprovider auth" error.

A typical host-side flow per provider:

1. User clicks "Connect MyProvider" in settings UI
   → host opens OAuth popup
2. Provider redirects to host callback URL with auth code
   → host exchanges code for { access_token, refresh_token }
3. Host stores the typed credentials in its auth store
   → keyed by (user_id, "myprovider", auth_id)
4. Future row dispatch resolves via UserAuth::sourced_credentials
   → host's match arm reads from the auth store
   → if access_token expired, host runs refresh, updates store,
     returns fresh credentials
5. Row body builds HTTP client from credentials, makes API
   calls, persists cells

The provider crate participates in steps 4–5 only; steps 1–3 are entirely host concern. Future work (see the gaps section in adding an integration) may collapse some of this into a connector-side OAuth helper crate.

Anti-patterns to avoid

  • Don't store credentials in Client. The grab-bag is a request-scoped thing; credentials should only ever be resolved via UserAuth so the host can rotate or revoke them.
  • Don't take &str AuthId in places that should be typed. The AuthId associated type exists so the row body can declare its expectations precisely. Keep &str confined to UserAuth::sourced_credentials (where it crosses the dyn boundary) and Params deserialization.