Disclaimer: These are planning documents. The functionalities described here may be unimplemented, partially implemented, or implemented differently than the original design.

Biome OAuth Integration

Summary

This RFC proposes adding a new Biome store for tracking OAuth sessions and an OAuth user identity with a Biome user ID. This change enables support of user notifications and signing key management on behalf of a user that has authenticated via an OAuth provider instead of Biome’s own credentials authentication.

Motivation

OAuth authentication provides Splinter with a way to integrate with other user management systems. These systems can include public providers, such as Github or Google, or internal single-sign-on installations that provide OAuth2 capabilities.

This new store will serve four purposes:

  • This store will provide a link between a Biome user ID and an OAuth user ID to connect an OAuth user to notification and keys. This is required by Canopy applications, where signing key management is provided by the Biome REST API endpoints that may be exposed by a Splinter daemon.

  • This store will enable the Splinter REST API to cache a user’s authorization so that it doesn’t need to check the authorization for each request. The store will track the last time the user was authenticated and, after some period of time, the REST API will re-authenticate the user.

  • This store will keep track of the refresh token associated with an OAuth session, which may be needed in the future to keep a user’s session going.

  • This store will allow the Splinter REST API to provide a custom access token that is associated with the OAuth access token. This is important because the Splinter REST API should not directly expose the OAuth access token for security reasons.

Guide-level explanation

This change will introduce a new store for saving the OAuth user session info. This OAuthUserSessionStore will manage the linkage between a user ID and the information about an OAuth user and their active sessions.

The information required for the OAuth user is:

  • Subject identifier: the unique identifier for the user within the OAuth2 provider’s system.
  • OAuth access token: an OAuth2 access token for validating the user’s authentication with the OAuth provider.
  • OAuth refresh token: an refresh token for getting a new access token from the OAuth provider (this is optional as not all OAuth providers have expiring access tokens).
  • Splinter access token: the access token created and issued by the Splinter REST API for the authenticated user’s session.

This information will be associated with a Biome user ID, which is a generated UUID.

Two initial implementations will be provided: one with in-memory storage, and another backed by a database via the Diesel crate.

Reference-level explanation

The OAuthUserSessionStore will be defined by the following trait, in the module splinter::biome::oauth::store:

use std::time::SystemTime;

/// Correlation between an OAuth user (subject) and a Biome user ID
pub struct OAuthUser {
    subject: String,
    user_id: String,
}

impl OAuthUser {
    // contents omitted for brevity
}

/// Data for an OAuth user's session that's in an [OAuthUserSessionStore]
pub struct OAuthUserSession {
    splinter_access_token: String,
    user: OAuthUser,
    oauth_access_token: String,
    oauth_refresh_token: Option<String>,
    last_authenticated: SystemTime,
}

impl OAuthUserSession {
    // accessor methods omitted for brevity

    /// Converts the session data into an update builder
    pub fn into_update_builder(
      self
    ) -> InsertableOAuthUserSessionUpdateBuilder {
        // contents omitted for brevity
    }
}

/// Builds a new [OAuthUserSession]
///
/// This builder should only be used by implementations of the
/// [OAuthUserSessionStore] for creating session data to return.
pub struct OAuthUserSessionBuilder {
    splinter_access_token: Option<String>,
    user: Option<OAuthUser>,
    oauth_access_token: Option<String>,
    oauth_refresh_token: Option<String>,
    last_authenticated: Option<SystemTime>,
}

impl OAuthUserSessionBuilder {
    // contents omitted for brevity
}

/// Data for an OAuth user's session that can be inserted into an
/// [OAuthUserSessionStore]
///
/// Unlike [OAuthUserSession], this struct does not contain a
/// `last_authenticated` timestamp or the user's Biome user ID; this is because
/// the timestamp and Biome user ID are always determined by the store itself.
pub struct InsertableOAuthUserSession {
    splinter_access_token: String,
    subject: String,
    oauth_access_token: String,
    oauth_refresh_token: Option<String>,
}

impl InsertableOAuthUserSession {
    // accessor methods omitted for brevity
}

/// Builds a new [InsertableOAuthUserSession]
pub struct InsertableOAuthUserSessionBuilder {
    splinter_access_token: Option<String>,
    subject: Option<String>,
    oauth_access_token: Option<String>,
    oauth_refresh_token: Option<String>,
}

impl InsertableOAuthUserSessionBuilder {
    // contents omitted for brevity
}

/// Builds an updated [InsertableOAuthUserSession]
///
/// This builder only allows changes to the fields of a session that may be
/// updated.
pub struct InsertableOAuthUserSessionUpdateBuilder {
    // Immutable items
    splinter_access_token: String,
    subject: String,
    // Mutable items
    oauth_access_token: String,
    oauth_refresh_token: Option<String>,
}

impl InsertableOAuthUserSessionUpdateBuilder {
    pub fn with_oauth_access_token(mut self, oauth_access_token: String) -> Self {
        self.oauth_access_token = oauth_access_token;
        self
    }

    pub fn with_oauth_refresh_token(mut self, oauth_refresh_token: Option<String>) -> Self {
        self.oauth_refresh_token = oauth_refresh_token;
        self
    }

    pub fn build(self) -> InsertableOAuthUserSession {
        // contents omitted for brevity
    }
}

/// Defines methods for CRUD operations on OAuth session data
pub trait OAuthUserSessionStore: Send + Sync {
    /// Adds an OAuth session
    ///
    /// The store will set the "last authenticated" value of the session to the
    /// current time. The store will also generate a new OAuth user entry if one
    /// does not already exist for the session's subject.
    ///
    /// # Errors
    ///
    /// Returns a `ConstraintViolation` error if a session with the given
    /// `splinter_access_token` already exists.
    fn add_session(
        &self,
        session: InsertableOAuthUserSession,
    ) -> Result<(), OAuthUserSessionStoreError>;

    /// Updates the OAuth access token and/or refresh token for a session
    ///
    /// The store will set the "last authenticated" value of the session to the
    /// current time.
    ///
    /// # Errors
    ///
    /// * Returns an `InvalidState` error if there is no session with the given
    ///   `splinter_access_token`
    /// * Returns a `InvalidArgument` error if any field other than
    ///   `oauth_access_token` or `oauth_refresh_token` have been changed.
    fn update_session(
        &self,
        session: InsertableOAuthUserSession,
    ) -> Result<(), OAuthUserSessionStoreError>;

    /// Removes an OAuth session based on the provided Splinter access token.
    ///
    /// # Errors
    ///
    /// Returns an `InvalidState` error if there is no session with the given
    /// `splinter_access_token`
    fn remove_session(&self, splinter_access_token: &str)
        -> Result<(), OAuthUserSessionStoreError>;

    /// Returns the OAuth session for the provided Splinter access token if it
    /// exists
    fn get_session(
        &self,
        splinter_access_token: &str,
    ) -> Result<Option<OAuthUserSession>, OAuthUserSessionStoreError>;

    /// Returns the correlation between the given OAuth subject identifier and a
    /// Biome user ID if it exists
    fn get_user(
      &self,
      subject: &str,
    ) -> Result<Option<OAuthUser>, OAuthUserSessionStoreError>;

    /// Clone into a boxed, dynamically dispatched store
    fn clone_box(&self) -> Box<dyn OAuthUserSessionStore>;
}

The database-backed implementation of this store will be comprised of two tables: the oauth_users table and the oauth_user_sessions table.

The oauth_users table will associate the user’s OAuth identity (the identity of the user according to the configured OAuth provider, such as a username or a subject identifier) with a unique Biome user ID. The table will be defined as:

CREATE TABLE IF NOT EXISTS oauth_users (
  subject                   TEXT        PRIMARY KEY,
  user_id                   TEXT        NOT NULL UNIQUE
);

The user_id value will be a UUID generated by the OAuthUser::new constructor. To ensure uniqueness of these user IDs with respect to user IDs generated by the Biome credentials store, these user IDs will be namespaced to this table.

The oauth_user_sessions table will track each active session that has been initiated by a user. The table will be defined as:

CREATE TABLE IF NOT EXISTS oauth_user_sessions (
  splinter_access_token    TEXT       PRIMARY KEY,
  subject                  TEXT       NOT NULL,
  oauth_access_token       TEXT       NOT NULL,
  oauth_refresh_token      TEXT,
  last_authenticated       BIGINT    DEFAULT extract(epoch from now()) NOT NULL,
  FOREIGN KEY (subject) REFERENCES oauth_users(subject) ON DELETE CASCADE
);

CREATE FUNCTION update_oauth_user_session_timestamp() RETURNS trigger AS $$
    BEGIN
      UPDATE oauth_user_sessions
      SET last_authenticated = extract(epoch from now())
      WHERE splinter_access_token = OLD.splinter_access_token;
    END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER oauth_user_sessions_timestamp_update
  AFTER UPDATE ON oauth_user_sessions
  FOR EACH ROW EXECUTE PROCEDURE update_oauth_user_session_timestamp();

Note that this is the PostgreSQL definition; the SQLite definition will differ slightly.

The splinter_access_token will be a value that is randomly generated by the Splinter REST API. The oauth_access_token and oauth_refresh_token will be used to periodically re-authenticate with the OAuth provider. The last_authenticated timestamp will be used to determine when the user should be re-authenticated with the OAuth provider (the Splinter REST API will determine when this needs to be done). This timestamp will be generated internally by the database with the oauth_user_sessions_timestamp_update trigger.

By separating the user definitions and sessions into separate tables, we are able to provide a stable user ID that can be correlated with other Biome tables (such as keys and user_notifications) while also allowing 0 or more active user sessions.

The storage of OAuth tokens in this Biome-specific store was intentionally designed. While the development team acknowledges that the contents of the oauth_user_sessions table are not user-specific (the same values would be used to represent a non-user OAuth session, such as one using the client credentials grant type), the table is contained within this store to keep the store self-contained. The team has deemed it undesirable for the Splinter stores to interact with external database tables. As such, the table has been explicitly named to indicate that it will only contain user sessions. If non-user OAuth authentication is implemented for Splinter, the tokens and other relevant data will be stored in a separate store and database table.

Drawbacks

None

Rationale and alternatives

This follows the standard biome store model.

Prior art

This follows the standard biome store model.

Unresolved Questions

None.