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)