feat: add JWT api - #39
Conversation
f6c5a97 to
966edda
Compare
knifecake
left a comment
There was a problem hiding this comment.
I think the direction is really good! Adopting OAuth tokens has always been a burden to companies that wanna create their own integrations because of the difficulty in doing the OAuth dance so I think this will be useful in increasing adoption of OAuth (and, more importantly, decrease adoption of API Keys which are less safe).
My only concern is our dependency on the access token format. Not all access tokens are JWTs, and even if we want them to be, we shouldn't depend on it. In particular, the exp claim in JWTs is only an upper bound: currently access tokens may be revoked at any time before their exp timestamp.
I know we have fallbacks and also parse the expires_in field of the /token endpoint response but IMO, a more robust approach would be to refresh once after a request fails due to an authentication error (which is mapped to HTTP status 401 in our public API), then retry the request with the updated token.
I'm not familiar with the API surface we're exposing in our SDKs but maybe it's worth building a request wrapper (if we don't already have one) and hide all of this inside it? (vs your proposed &block approach?) Some other benefits I see around this is that its more standard across languages (JS has callbacks and Python has withs but they seem a bit hacky to me), and that we can model other transport restrictions the API may impose (rate limits) or benefit from (connection reuse).
Also, regarding refresh token reuse, I really like that we're supporting both flows, because we're rebuilding authentication for the Public API in Factorial ID which doesn't reuse refresh tokens.
|
Agreed, expiry can't see revocation, so I've added the reactive path. The SDK already had a request wrapper seat: every generated method funnels through One note on the |
🚪 Why?
The Ruby SDK only accepts static credentials (
api_key/token). Any OAuth2 integration has to hand-roll the entire token lifecycle: exchanging the authorization code, decoding the JWT to know when it expires, refreshing proactively, and surviving single-use refresh-token rotation (if you lose the rotated token, the chain breaks).🔑 What?
F::Token— read-only view over a bearer credential that may be a JWT:claims,expires_at,expired?,expiring_soon?(margin:). Decodes without verifying (verification is the server's job — signature/JWKS stays infactorial-auth, out of scope). Opaque tokens degrade gracefully. Stdlib-only.F::OAuth—authorize_url,exchange_code,refreshagainst/oauth/token, returning immutableF::OAuth::Tokens. Failures raiseF::OAuthError(code/body), mirroringF::ApiError.F::OAuth::Session— self-refreshing, thread-safe token holder with a mandatory rotation callback (persisting the new refresh token is not optional). Wired intoF::Api.new(oauth: session)through the generated client'saccess_token_getterhook, evaluated on every request — no generated code touched, no new dependencies.F::Api.newwithout any credential now raisesArgumentError(matching TS/Python) instead of sending unauthenticated requests; empty-string env vars count as absent.oauth_token.rbis a thin CLI overF::OAuth), README documents the flow, and 60 handwritten specs run over a real TCP socket — including an end-to-end one where an about-to-expire token triggers a mid-flight refresh and the API request carries the rotated bearer.