Skip to content

Releases: rarebit-one/standard_id

v0.29.1

Choose a tag to compare

@github-actions github-actions released this 15 Jul 17:31
d01b188

Fixed

  • Signing in during an OAuth flow returned a 500 when use_inertia is on.
    With config.use_inertia = true, the WebEngine's auth pages are Inertia
    components, so their form submits are Inertia XHRs. The three
    post-authentication redirects — passwordless OTP verify, password login, and
    signup — used a plain redirect_to to send the user on to whatever was
    stashed in session[:return_to_after_authenticating]. When that destination
    is a non-Inertia controller (the ApiEngine's /api/authorize in an OAuth
    round-trip), the Inertia client follows the redirect with the X-Inertia
    header still attached, and inertia_rails' middleware raises
    NoMethodError: undefined method 'inertia_configuration' — a 500 that broke
    MCP/OAuth sign-in outright.

    The root asymmetry: inertia_rails mixes its controller module in via
    on_load(:action_controller_base), so Web::BaseController
    (ActionController::Base) gets inertia_configuration while
    Api::BaseController (ActionController::API) never does.

    All three sites now route through a shared redirect_after_authentication
    helper on Web::BaseController, which uses the existing
    InertiaSupport#redirect_with_inertia — already used for social login and
    signup — to emit a 409 + X-Inertia-Location so the browser performs a real
    page visit. Note this is keyed on the request being Inertia, not on the
    destination being cross-origin: the destination in the OAuth case is
    same-origin, so an external-only check would not have fixed it.

    The flash notice is now written before redirecting so it survives the
    Inertia branch (which cannot carry redirect_to's notice: option).

  • Post-auth redirects to allow-listed deep links raised
    UnsafeRedirectError.
    safe_destination? admits configured non-HTTP
    schemes (e.g. myapp://), but redirect_to rejects them without
    allow_other_host:. Now passed conditionally — mirroring
    ProvidersController — so Rails' same-origin backstop still applies to
    everything else. safe_destination? itself is unchanged.

v0.29.0

Choose a tag to compare

@github-actions github-actions released this 14 Jul 18:30
b8037fa

Fixed

  • A rate-limited GET no longer drives an unbounded redirect loop. The shared
    rate-limit handler bounced every tripped action to request.path. That is
    safe for a POST/PATCH (its sibling GET is a different action), but v0.28.0
    shipped the first rate-limited GETs — the email/phone verification-code
    confirm #show actions — so a tripped GET redirected to itself: the
    browser followed the redirect, re-incremented the counter, and got redirected
    again, an unbounded loop the victim's browser drives that also permanently
    resets the window. The handler now renders a terminal 429 Too Many Requests
    on GET/HEAD (no redirect), and keeps the existing same-path redirect for
    non-GET web actions. API responses are unchanged.
  • The Retry-After header now reflects the tripped limit's real window. It
    was hardcoded to 15 minutes, which was 4x too short for every 1-hour limit
    (verification_start_per_ip, password_reset_start_per_ip, signup_per_ip,
    api_passwordless_start_per_ip, dynamic_registration_per_ip). Each
    rate_limit declaration's within: is now threaded to the handler (captured
    in the per-limit with: closure the instant that limit trips, since
    ActionController::TooManyRequests carries no window and a controller may
    declare several limits with different windows), so Retry-After matches the
    window that actually fired. A hand-rolled raise that bypasses the macro
    falls back to 15 minutes.
  • Blank per-target rate-limit keys no longer collapse into one shared bucket.
    Five per-target limiters interpolated a param that can be blank, yielding a
    stable key like "reset-password:" — a single global bucket (Rails'
    .compact does not drop a non-nil empty string), so blank-target spam from
    one source throttled every legitimate user. Affected: web login (by email),
    API passwordless start (by target), email/phone verification start (by
    target), and password-reset start (by email). Each now falls the key back to
    the remote IP when the target is blank, keeping blank spam bounded per-IP
    without poisoning real targets' buckets — mirroring the existing per-audience
    token limiter's "only count well-formed values" shape.

Added

  • Mechanism-agnostic login rate-limit config: rate_limits.login_per_ip
    (default 20) and rate_limits.login_per_email (default 5).
    The login
    #create action branches password OR passwordless, so on a passwordless app
    the existing password_login_per_* fields actually govern the OTP-send limit
    — a misnomer that led four consumer apps to write false config comments. The
    new names describe the action, not a mechanism. When unset they inherit the
    deprecated password_login_* values, so existing behaviour is unchanged; when
    set they win.

Deprecated

  • rate_limits.password_login_per_ip / rate_limits.password_login_per_email
    are deprecated in favour of login_per_ip / login_per_email.
    They remain
    fully honoured (the config schema rejects unknown fields at boot, so a host
    still setting the old names keeps working); the login controller reads the new
    alias and falls back to the deprecated field when the alias is left at its
    default. Follows the max_attemptsmax_attempts_per_challenge alias
    precedent.

v0.28.0

Choose a tag to compare

@github-actions github-actions released this 12 Jul 13:01
989f978

Added

  • Rate limiting on the last unprotected auth surfaces. The web
    password-reset request (reset_password/start), password signup, and the
    email/phone code-confirmation endpoints now carry Rails-native
    rate_limits, closing email-flooding, account-enumeration,
    account-creation-spam, and distributed code-guessing gaps. New config keys:
    rate_limits.password_reset_start_per_ip (10/hr),
    rate_limits.password_reset_start_per_target (3/15min), and
    rate_limits.signup_per_ip (10/hr); the confirm endpoints reuse
    otp_verify_per_ip. All use the existing RateLimitStore + centralized 429
    handler.
  • The passwordless.retry_delay OTP-resend cooldown is now enforced.
    Previously the setting existed but did nothing. A resend for the same target
    within the window (default 30s) is rejected with an InvalidRequestError;
    the already-issued code stays valid. Set retry_delay = 0 to disable.

v0.27.0

Choose a tag to compare

@github-actions github-actions released this 03 Jul 03:20
9fd80c1

Added

  • Loopback redirect URIs match on any port for public PKCE clients (RFC 8252
    §7.3).
    Native apps receive the authorization response on an ephemeral
    local listener whose port cannot be known at registration time. When a
    public client with require_pkce presents a redirect URI whose scheme is
    http and whose host is a loopback literal (127.0.0.1, ::1, or
    localhost), and a registered redirect URI is likewise a loopback URI, the
    comparison now ignores the port — host and path must still match exactly, so
    127.0.0.1 and localhost do not cross-match (RFC 8252 §8.3 recommends the
    IP literals over localhost). Confidential clients and non-loopback URIs
    keep strict scheme+host+port+path matching. Registration-time validation is
    unchanged. Token exchange is unaffected: the redirect URI presented at the
    token endpoint is still compared byte-for-byte against the value stored when
    the code was issued.

v0.26.4

Choose a tag to compare

@github-actions github-actions released this 22 Jun 06:48
765178e

Fixed

  • WebEngine controller redirects no longer drop the mount prefix on non-root
    mounts.
    Isolated-engine _path helpers are mount-relative, and
    redirect_to / redirect_with_inertia (unlike form_with / url_for view
    URL generation) do not prepend the mount's SCRIPT_NAME. So when the engine
    was mounted at a non-root path (e.g. mount StandardId::WebEngine => "/auth"),
    redirects produced prefix-less paths that 404'd — most visibly POST /auth/login redirecting to /login_verify instead of /auth/login_verify,
    breaking passwordless login. Form actions were never affected. All affected
    redirects (loginlogin_verify; login_verify / reset-password →
    login; session revoke → sessions; account update → account; and the
    after_sign_in denial bounce) now prepend request.script_name via a new
    engine_path helper (a no-op for root mounts). Regression test added to
    spec/integration/multi_mount_spec.rb.

v0.26.3

Choose a tag to compare

@github-actions github-actions released this 15 Jun 05:41
097252b

Fixed

  • Unauthenticated access to a protected web page no longer 500s. The web
    authentication guard (require_browser_session!) raises
    NotAuthenticatedError / InvalidSessionError (for missing / expired /
    revoked sessions) rather than redirecting. The API base controller rescued
    these, but the web base controller did not — so an unauthenticated request to
    a protected web page (e.g. /sessions) surfaced as a 500 instead of bouncing
    to login. The web base controller now rescues both and redirects to the login
    page, preserving the original destination.

v0.26.2

Choose a tag to compare

@github-actions github-actions released this 15 Jun 04:24
e8b1420

Security

  • OTP codes and password-reset tokens no longer leak into the logs. The
    built-in mailers (PasswordlessMailer, PasswordResetMailer) pass the OTP
    code / reset URL as mailer params, which deliver_later serializes as the
    delivery job's arguments — and ActiveJob's log subscriber prints job arguments
    in plaintext on enqueue/perform (e.g. params: {email:…, otp_code: "03158369"}).
    StandardId's mailers now deliver via StandardId::SecureMailDeliveryJob
    (ActionMailer::MailDeliveryJob with log_arguments = false), so the
    arguments are kept out of the log stream. Delivery behaviour is unchanged.

v0.26.1

Choose a tag to compare

@github-actions github-actions released this 15 Jun 02:00
d237b56

Fixed

  • Passwordless login_verify OTP input now respects
    config.passwordless.code_length.
    The built-in ERB verification-code field
    hardcoded maxlength: 6, so apps configuring a longer code (e.g.
    code_length = 8) rendered a field that truncated input to 6 characters —
    users could not enter the full code. The input now derives maxlength from
    StandardId::Passwordless.otp_code_length (the same clamped 4..10 value the
    OTP generator uses), exposed to views via a new
    StandardId::ApplicationHelper#otp_code_length helper, so the form and the
    generated code stay in sync end-to-end.
  • WebEngine rate-limit responses no longer 500 on hosts without a root
    route.
    When a web auth action (login, login_verify, email/phone verification
    start) hit its rate limit, the handler redirected to
    request.referer || main_app.root_path. That raised — and returned a 500
    error page instead of the intended graceful response — for any host app that
    doesn't define a root route (e.g. an API/control-plane that only mounts the
    engine), and also for cross-origin Referer headers (Rails' open-redirect
    guard). The handler now redirects back to the rate-limited form's own path
    (request.path), which is always a valid same-origin GET. The ApiEngine
    responses (JSON 429) are unchanged.
  • OAuth/OIDC metadata no longer advertises a jwks_uri under symmetric
    signing.
    With the default HS256 (and HS384/HS512) there are no public keys
    to publish, so the JWKS endpoint deliberately returns 404 — but the
    authorization-server and openid-configuration documents advertised jwks_uri
    unconditionally, pointing clients at a dead URL. jwks_uri is now emitted only
    when signing is asymmetric (RS*/ES*). RFC 8414 makes it optional; HS-signed
    tokens are verified with the shared secret, not JWKS.
  • Sign-out / unauthenticated requests no longer leave a non-HttpOnly
    session_token cookie.
    clear_session! assigned
    cookies.encrypted[:session_token] = nil, which wrote a fresh encrypted blob
    through the cookie jar's default options (no HttpOnly) on every
    unauthenticated request. It now uses cookies.delete(:session_token) to remove
    the cookie cleanly. The token-bearing sign-in write was already httponly: true
    and is unchanged, so this is a hygiene/consistency fix, not a token exposure.

v0.26.0

Choose a tag to compare

@github-actions github-actions released this 14 Jun 16:58
54944a3

Added

  • config.passwordless.production_env_detector — an optional callable that
    decides whether the current deploy counts as "production" for the bypass-code
    guard. When nil (default) the gem falls back to Rails.env.production?, so
    existing consumers are unchanged. Apps that distinguish a physical deploy
    environment from RAILS_ENV (e.g. a staging box still running
    RAILS_ENV=production) can supply -> { AppEnv.production? } to permit a
    bypass_code on staging while it stays refused on real production. The guard
    in Passwordless::VerificationService (which also backs Otp.verify) now
    defers to this detector instead of checking Rails.env.production? directly.

v0.25.0

Choose a tag to compare

@github-actions github-actions released this 13 Jun 05:47
15f78bd

Changed

  • Browser session cookie now persists across browser restarts. The
    encrypted session_token cookie is written with an explicit expires tied
    to the BrowserSession#expires_at (previously a bare session cookie that was
    cleared on full browser close, logging users out well before their session
    actually expired). The cookie is also hardened with httponly: true,
    same_site: :lax, and secure following request.ssl?. Combined with a
    host-configured session.browser_session_lifetime, this lets a "remember me
    for N days" session survive closing and reopening the browser. Applies to
    both the sign-in and remember-token re-auth paths.