DEV Community

Cover image for πŸ’Ž ANN: oauth2 v2.0.13
Peter H. Boling
Peter H. Boling

Posted on • Edited on

πŸ’Ž ANN: oauth2 v2.0.13

πŸ’Ž ANN: oauth2 v2.0.13 supporting token revocation via URL-encoded params, complete examples, YARD docs, & RBS types. Used by > 100k other projects, downloaded millions of times per week, w/ 0 backers, & 0 sponsors, and a brand new open collective to change that! (holds out hand). Comprehensive examples below the fold.

πŸ”¨ github.com/ruby-oauth/oauth2

πŸ’³ opencollective.com/ruby-oauth (more funding options at the bottom!)

πŸ“˜ Comprehensive Usage

Common Flows (end-to-end)

  • Authorization Code (server-side web app):
require "oauth2"
client = OAuth2::Client.new(
  ENV["CLIENT_ID"],
  ENV["CLIENT_SECRET"],
  site: "https://provider.example.com",
  redirect_uri: "https://my.app.example.com/oauth/callback",
)

# Step 1: redirect user to consent
state = SecureRandom.hex(16)
auth_url = client.auth_code.authorize_url(scope: "openid profile email", state: state)
# redirect_to auth_url

# Step 2: handle the callback
# params[:code], params[:state]
raise "state mismatch" unless params[:state] == state
access = client.auth_code.get_token(params[:code])

# Step 3: call APIs
profile = access.get("/api/v1/me").parsed
Enter fullscreen mode Exit fullscreen mode
  • Client Credentials (machine-to-machine):
client = OAuth2::Client.new(ENV["CLIENT_ID"], ENV["CLIENT_SECRET"], site: "https://provider.example.com")
access = client.client_credentials.get_token(audience: "https://api.example.com")
resp = access.get("/v1/things")
Enter fullscreen mode Exit fullscreen mode
  • Resource Owner Password (legacy; avoid when possible):
access = client.password.get_token("jdoe", "s3cret", scope: "read")
Enter fullscreen mode Exit fullscreen mode

Refresh Tokens

When the server issues a refresh_token, you can refresh manually or implement an auto-refresh wrapper.

  • Manual refresh:
if access.expired?
  access = access.refresh
end
Enter fullscreen mode Exit fullscreen mode
  • Auto-refresh wrapper pattern:
class AutoRefreshingToken
  def initialize(token_provider, store: nil)
    @token = token_provider
    @store = store # e.g., something that responds to read/write for token data
  end

  def with(&blk)
    tok = ensure_fresh!
    blk ? blk.call(tok) : tok
  rescue OAuth2::Error => e
    # If a 401 suggests token invalidation, try one refresh and retry once
    if e.response && e.response.status == 401 && @token.refresh_token
      @token = @token.refresh
      @store.write(@token.to_hash) if @store
      retry
    end
    raise
  end

private

  def ensure_fresh!
    if @token.expired? && @token.refresh_token
      @token = @token.refresh
      @store.write(@token.to_hash) if @store
    end
    @token
  end
end

# usage
keeper = AutoRefreshingToken.new(access)
keeper.with { |tok| tok.get("/v1/protected") }
Enter fullscreen mode Exit fullscreen mode

Persist the token across processes using AccessToken#to_hash and AccessToken.from_hash(client, hash).

Token Revocation (RFC 7009)

You can revoke either the access token or the refresh token.

# Revoke the current access token
access.revoke(token_type_hint: :access_token)

# Or explicitly revoke the refresh token (often also invalidates associated access tokens)
access.revoke(token_type_hint: :refresh_token)
Enter fullscreen mode Exit fullscreen mode

Client Configuration Tips

  • Authentication schemes for the token request:
OAuth2::Client.new(
  id,
  secret,
  site: "https://provider.example.com",
  auth_scheme: :basic_auth, # default. Alternatives: :request_body, :tls_client_auth, :private_key_jwt
)
Enter fullscreen mode Exit fullscreen mode
  • Faraday connection, timeouts, proxy, custom adapter/middleware:
client = OAuth2::Client.new(
  id,
  secret,
  site: "https://provider.example.com",
  connection_opts: {
    request: {open_timeout: 5, timeout: 15},
    proxy: ENV["HTTPS_PROXY"],
    ssl: {verify: true},
  },
) do |faraday|
  faraday.request(:url_encoded)
  # faraday.response :logger, Logger.new($stdout) # see OAUTH_DEBUG below
  faraday.adapter(:net_http_persistent) # or any Faraday adapter you need
end
Enter fullscreen mode Exit fullscreen mode
  • Redirection: The library follows up to max_redirects (default 5). You can override per-client via options[:max_redirects].

Handling Responses and Errors

  • Parsing:
resp = access.get("/v1/thing")
resp.status     # Integer
resp.headers    # Hash
resp.body       # String
resp.parsed     # SnakyHash::StringKeyed or Array when JSON array
Enter fullscreen mode Exit fullscreen mode
  • Error handling:
begin
  access.get("/v1/forbidden")
rescue OAuth2::Error => e
  e.code         # OAuth2 error code (when present)
  e.description  # OAuth2 error description (when present)
  e.response     # OAuth2::Response (full access to status/headers/body)
end
Enter fullscreen mode Exit fullscreen mode
  • Disable raising on 4xx/5xx to inspect the response yourself:
client = OAuth2::Client.new(id, secret, site: site, raise_errors: false)
res = client.request(:get, "/v1/maybe-errors")
if res.status == 429
  sleep res.headers["retry-after"].to_i
end
Enter fullscreen mode Exit fullscreen mode

Making Raw Token Requests

If a provider requires non-standard parameters or headers, you can call client.get_token directly:

access = client.get_token({
  grant_type: "client_credentials",
  audience: "https://api.example.com",
  headers: {"X-Custom" => "value"},
  parse: :json, # override parsing
})
Enter fullscreen mode Exit fullscreen mode

OpenID Connect (OIDC) Notes

  • If the token response includes an id_token (a JWT), this gem surfaces it but does not validate the signature. Use a JWT library and your provider's JWKs to verify it.
  • For private_key_jwt client authentication, provide auth_scheme: :private_key_jwt and ensure your key configuration matches the provider requirements.

Debugging

  • Set environment variable OAUTH_DEBUG=true to enable verbose Faraday logging (uses the client-provided logger).
  • To mirror a working curl request, ensure you set the same auth scheme, params, and content type. The Quick Example at the top of the README.md shows a curl-to-ruby translation.

Support & Funding Info

I am a full-time FLOSS maintainer. If you find my work valuable I ask that you become a sponsor. Every dollar helps!

πŸ₯° Support FLOSS work πŸ₯° Get access "Sponsors" channel on Galtzo FLOSS Discord πŸ‘‡οΈ Live Chat on Discord
OpenCollective Backers OpenCollective Sponsors Buy me a coffee Donate at ko-fi.com Donate on PayPal Donate on Polar Sponsor Me on Github Liberapay Goal Progress

Cover photo (cropped) by Klara Kulikova on Unsplash

Top comments (0)