-
Notifications
You must be signed in to change notification settings - Fork 1
Auth
Principal+delegate identity for tep apps. Apps configure one or
more credential providers; every request gets a req.identity
populated (anonymous if no provider matched), and handler code
reads identity uniformly regardless of how the caller
authenticated.
Battery 1 in docs/BATTERIES-DESIGN.md.
class Tep::Identity
attr_reader :principal_id # String, opaque (apps own format)
attr_reader :acting_via # Tep::AgentDelegation or nil
attr_reader :capabilities # Array of Symbols
def human? ; @acting_via == nil end
def agent? ; !human? end
def may?(cap) ; @capabilities.include?(cap) end
def subject ; ... end # "user:42" for humans;
# "agent:bot/user:42" for agents
end
class Tep::AgentDelegation
attr_reader :agent_id, :issued_at, :expires_at, :origin
endreq.identity is always non-nil. Anonymous requests get
Tep::Identity.anonymous (empty principal_id, empty caps, no
delegation). Handlers gate via if req.identity.may?(:write) or
if req.identity.human?.
Configure once at boot, install the filter, done:
require 'sinatra'
Tep::AuthBearerToken.set_secret(ENV["JWT_SECRET"])
Tep.session_secret = ENV["TEP_SESSION_SECRET"]
Tep::Auth.install!
get '/me' do
res.headers["Content-Type"] = "text/plain"
req.identity.subject
endThree providers ship, chained in fixed order — first match wins:
-
Tep::AuthBearerToken —
Authorization: Bearer <JWT>. HS256. -
Tep::AuthSessionCookie — reads identity from
req.session(the existingtep.sessionsigned cookie). Apps write the identity on login + clear on logout:post '/login' do # ... password / OAuth check ... ident = Tep::Identity.new("user:42", nil, [:read, :write]) Tep::AuthSessionCookie.set(req, ident, 0) # 0 = no exp "" end post '/logout' do Tep::AuthSessionCookie.clear(req) "" end
-
Tep::AuthOAuth2 — authorization-code grant issuance (tep is the auth server here, not a client). Used for bots / agents acting on behalf of a human. See the section below.
The provider chain order is hardcoded (spinel's
PtrArray<Base> dispatch can't carry cls_id reliably). To skip
a provider, don't configure its secret — Bearer with no secret
returns nil from try, falling through to the next.
Flat single-level JSON (matches Tep::Json's flat-object
extractors):
{
"sub": "user:42",
"exp": 1716396000,
"caps": "read,write,post_summary",
"delegate": "summarizer-bot|1716392400|1716396000|token"
}delegate is pipe-encoded agent_id|issued_at|expires_at|origin
— optional. When present, req.identity.agent? is true and
req.identity.acting_via.agent_id is the agent's id.
For bots / agents getting tokens on behalf of a human:
Tep::AuthOAuth2.register_client(
"summarizer-bot",
"Summarizer Bot",
"https://bot.example/oauth/callback",
[:read, :post_summary])
# In your /authorize route, after consent UI + user clicks Allow:
code = Tep::AuthOAuth2.issue_code(
req.identity.principal_id,
"summarizer-bot",
"read,post_summary",
0) # 0 = DEFAULT_CODE_TTL (600s)
# Bot's /token route redeems for a JWT:
token = Tep::AuthOAuth2.exchange_code(
params[:code], params[:client_id], 0)
# Bot uses `Authorization: Bearer <token>` from here.The exchanged JWT carries delegate populated with
agent_id=<client_id>, origin=:oauth_grant. Downstream
identity surface is identical — the bot's req.identity.agent?
is true, acting_via.agent_id is the client_id.
Tep::Auth::CORE_CAPABILITIES = [:read, :write, :authn, :authz].
Apps register their own via Tep::Auth.register_capability(:foo)
(planned follow-up; today caps are just symbols apps choose).
The granted cap set on a token is always a subset of the
principal's caps — the issuer enforces it at consent time.
may?(cap) checks the set; apps gate authz:
get '/admin' do
raise "denied" unless req.identity.may?(:authz)
# ...
endTep::Auth.install! registers Tep::AuthFilter in a dedicated
@auth_filter slot on Tep::App — runs before the user's
@before_filter. So user filters and handlers always see a
populated req.identity.