Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions migration/db/migrations/20260519100000000_add_oauth_tokens.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
-- +micrate Up
-- SQL in section 'Up' is executed when this migration is applied

-- ---------------------------------------------------------------------------
-- OAuthToken: per-token metadata for issued OAuth2 access + refresh tokens.
--
-- Used by the auth.cr service (replacing the legacy Ruby Doorkeeper
-- service) for two purposes:
-- 1. Token revocation — `revoked_at` flips from NULL to a unix
-- timestamp on revoke. The authly shard's TokenStore checks
-- revoked_at on every Bearer-token validation.
-- 2. Audit / introspection — `/auth/introspect` returns the issuing
-- client, sub, scope, and lifetime.
--
-- Token IDs (`jti`) are 64 hex chars (Random::Secure.hex(32)). Most
-- fields are nullable so we can record a "revoke first, fill in
-- details if we later see the row" pattern — important because authly
-- generates refresh tokens without calling `store_token_metadata`
-- (refresh tokens are stateless JWTs even when `persist_jwt_tokens`
-- is on); revoking such a refresh token still needs to leave a marker.
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS "oauth_tokens"(
id bigserial PRIMARY KEY,
jti character varying NOT NULL,
token_type character varying,
client_id character varying,
sub character varying,
scope character varying,
issued_at bigint,
expires_at bigint,
cert_thumbprint character varying,
revoked_at bigint,
created_at timestamp(6) without time zone NOT NULL,
updated_at timestamp(6) without time zone NOT NULL
);

CREATE UNIQUE INDEX IF NOT EXISTS index_oauth_tokens_on_jti ON "oauth_tokens" USING btree (jti);
CREATE INDEX IF NOT EXISTS index_oauth_tokens_on_expires_at ON "oauth_tokens" USING btree (expires_at);

-- +micrate Down
DROP TABLE IF EXISTS "oauth_tokens";
42 changes: 42 additions & 0 deletions src/placeos-models/oauth_token.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
require "./base/model"

module PlaceOS::Model
# Persistence backing for the auth.cr `AuthlyAdapter::TokenStore`.
# Each row records the metadata of a single OAuth2 access or refresh
# token; the `revoked_at` column lets the auth service mark a token
# invalid without rotating signing keys.
#
# See migration `20260519100000000_add_oauth_tokens.sql` for the
# column rationale (notably: most columns are nullable so a revoke
# for a never-stored refresh token can still leave a marker).
class OAuthToken < ModelWithAutoKey
table :oauth_tokens

attribute jti : String
attribute token_type : String? = nil
attribute client_id : String? = nil
attribute sub : String? = nil
attribute scope : String? = nil
attribute issued_at : Int64? = nil
attribute expires_at : Int64? = nil
attribute cert_thumbprint : String? = nil
attribute revoked_at : Int64? = nil

ensure_unique :jti

validates :jti, presence: true

# `true` if the token has been marked revoked via `revoked_at`.
def revoked? : Bool
!@revoked_at.nil?
end

# Stamps `revoked_at` to the current time (epoch seconds) and
# saves. No-op if already revoked.
def revoke! : Nil
return if revoked?
self.revoked_at = Time.utc.to_unix
save!
end
end
end
Loading