simple_oauth
Simply builds and verifies OAuth 1.0 headers per RFC 5849, and builds OAuth 2.0 requests per RFC 6749, RFC 7636, and RFC 7009.
Neither makes HTTP requests: you send what it builds with the HTTP client of your choice.
Installation
Install the gem and add to the application's Gemfile by executing:
$ bundle add simple_oauth
If bundler is not being used to manage dependencies, install the gem by executing:
$ gem install simple_oauth
Usage
Building an OAuth Header
require "simple_oauth"
header = SimpleOAuth::Header.new(
:get,
"https://api.example.com/resource",
{status: "Hello"},
consumer_key: "consumer_key",
consumer_secret: "consumer_secret",
token: "access_token",
token_secret: "token_secret"
)
header.to_s
# => "OAuth oauth_consumer_key=\"consumer_key\", oauth_nonce=\"...\", ..."
Signing a Request
Header.from_request takes the method, URL, and parameters from a request object, such as a Net::HTTPRequest. Query parameters are always signed, a form-encoded body is signed as parameters, and any other body is hashed into oauth_body_hash:
request = Net::HTTP::Post.new(URI("https://api.example.com/statuses"))
request.set_form_data(status: "Hello")
request["Authorization"] = SimpleOAuth::Header.from_request(request,
consumer_key: "key",
consumer_secret: "secret"
).to_s
Repeated Parameters
Pass an Array of values, or an Array of key-value pairs, when a key repeats:
header = SimpleOAuth::Header.new(:post, url, {"ids" => %w[1 2]},
consumer_key: "key",
consumer_secret: "secret"
)
header = SimpleOAuth::Header.new(:post, url, [["ids", "1"], ["ids", "2"]],
consumer_key: "key",
consumer_secret: "secret"
)
Signature Methods
Built-in signature methods: HMAC-SHA1 (default), HMAC-SHA256, RSA-SHA1, RSA-SHA256, and PLAINTEXT.
# Using HMAC-SHA256
header = SimpleOAuth::Header.new(:get, url, params,
consumer_key: "key",
consumer_secret: "secret",
signature_method: "HMAC-SHA256"
)
# Using RSA-SHA1 (pass PEM-encoded private key as consumer_secret)
header = SimpleOAuth::Header.new(:get, url, params,
consumer_key: "key",
consumer_secret: File.read("private_key.pem"),
signature_method: "RSA-SHA1"
)
Custom Signature Methods
Register custom signature methods at runtime:
SimpleOAuth::Signature.register("HMAC-SHA512") do |secret, signature_base|
Base64.encode64(OpenSSL::HMAC.digest("SHA512", secret, signature_base)).delete("\n")
end
# Check registered methods
SimpleOAuth::Signature.registered?("HMAC-SHA512") # => true
SimpleOAuth::Signature.registered_methods
# => ["hmac_sha1", "hmac_sha256", "rsa_sha1", "rsa_sha256", "plaintext", "hmac_sha512"]
OAuth Request Body Hash
For non-form-encoded request bodies (e.g., JSON), pass the body as the fifth parameter to compute oauth_body_hash, which is hashed with the signature method's algorithm. Form-encoded bodies are signed by passing their parameters as params instead.
json_body = '{"text": "Hello, World!"}'
header = SimpleOAuth::Header.new(:post, url, {},
{consumer_key: "key", consumer_secret: "secret"},
json_body
)
Realm Parameter
Include a realm in the Authorization header:
header = SimpleOAuth::Header.new(:get, url, params,
consumer_key: "key",
consumer_secret: "secret",
realm: "Example"
)
# => "OAuth realm=\"Example\", oauth_consumer_key=\"key\", ..."
Parsing OAuth Headers
Parse an OAuth Authorization header:
parsed = SimpleOAuth::Header.parse('OAuth oauth_consumer_key="key", oauth_signature="sig"')
# => {consumer_key: "key", signature: "sig"}
Parse OAuth credentials from a form-encoded POST body, or from a query string:
parsed = SimpleOAuth::Header.parse_form_body('oauth_consumer_key=key&oauth_signature=sig&status=hello')
# => {consumer_key: "key", signature: "sig"}
parsed = SimpleOAuth::Header.parse_query("oauth_consumer_key=key&status=hello")
# => {consumer_key: "key"}
Verifying Signatures
# Parse incoming Authorization header
header = SimpleOAuth::Header.new(:get, request_url, params, )
# Verify the signature
header.valid?(consumer_secret: "secret", token_secret: "token_secret")
# => true
Verifying compares signatures in constant time and leaves the header's own options untouched, so the secrets stay with the caller.
RSA signatures verify with the client's public key, which is all a server has:
header.valid?(consumer_secret: File.read("client_public_key.pem"))
Custom signature methods that cannot be verified by recomputing the signature register a verify block:
SimpleOAuth::Signature.register("RSA-SHA512", rsa: true,
verify: ->(key, signature_base, signature) {
OpenSSL::PKey::RSA.new(key).verify("SHA512", SimpleOAuth::Signature.decode_base64(signature), signature_base)
}) do |private_key_pem, signature_base|
SimpleOAuth::Signature.encode_base64(OpenSSL::PKey::RSA.new(private_key_pem).sign("SHA512", signature_base))
end
OAuth 2.0
SimpleOAuth::OAuth2::Client builds authorization URLs and the requests for its token and revocation endpoints. Each request is a SimpleOAuth::OAuth2::Request with a method, url, headers, and form-encoded body, ready to send with any HTTP client.
A client with a secret is confidential and authenticates with HTTP Basic, or in the request body with auth_method: :client_secret_post. A client without a secret is public and sends only its client_id.
authorization_url names pkce rather than defaulting it, because only the caller can keep the verifier to send with the code. OAuth 2.1 asks every client for a PKCE challenge, public and confidential alike, so leaving it out is a decision you make in writing:
# An OAuth 2.0 authorization server that rejects the challenge parameters
client.(redirect_uri: "https://app.example/callback", pkce: nil, state: state)
A PKCE challenge ties the authorization response to the request, which is what state does under OAuth 2.0. With a challenge the state is yours to use for application state, or to leave out; without one it is required.
Authorization Code Flow with PKCE
require "net/http"
require "simple_oauth"
client = SimpleOAuth::OAuth2::Client.new(
client_id: "client_id",
client_secret: "client_secret", # omit for a public client
authorization_endpoint: "https://x.com/i/oauth2/authorize",
token_endpoint: "https://api.x.com/2/oauth2/token",
revocation_endpoint: "https://api.x.com/2/oauth2/revoke"
)
# 1. Send the user to authorize the client
pkce = SimpleOAuth::OAuth2::PKCE.generate
state = SecureRandom.hex
redirect_to client.(
redirect_uri: "https://app.example/callback",
pkce: pkce,
state: state,
scope: %w[tweet.read users.read offline.access]
)
# 2. Read the response the authorization server returned to the callback
response = SimpleOAuth::OAuth2::AuthorizationResponse.parse(request.query_string, state: state)
# 3. Exchange the code for a token
request = client.(
code: response.code,
redirect_uri: "https://app.example/callback",
code_verifier: pkce.verifier
)
response = Net::HTTP.post(URI(request.url), request.body, request.headers)
token = SimpleOAuth::OAuth2::Token.from_response(status: response.code, body: response.body)
token.access_token # => "..."
token.refresh_token # => "..."
token.expires_at # => 2026-09-11 14:00:00 +0000
AuthorizationResponse.parse makes the checks a client owes its own request, raising SimpleOAuth::OAuth2::Error rather than returning a code you cannot trust:
- the error the server reported, if it reported one (RFC 6749 Section 4.1.2.1)
- a
statethat is not the one the authorization URL sent, compared in constant time - an
issthat is not the expected issuer, when one is given (RFC 9207, which defends against a mix-up between authorization servers) - a parameter the response repeats, which RFC 6749 Section 3.1 forbids
- a response carrying no code at all
SimpleOAuth::OAuth2::AuthorizationResponse.parse(
request.query_string,
state: session[:state], # omit when the request sent none
issuer: "https://server.example.com" # omit to make no issuer check
)
It takes the query string, or the parameters a framework already parsed. Token.from_response raises SimpleOAuth::OAuth2::Error for an error response, with the endpoint's code, description, uri, and HTTP status.
Refreshing, Client Credentials, and Revocation
client.refresh_token_request(refresh_token: token.refresh_token)
client.client_credentials_request(scope: "read") # confidential clients only
client.revocation_request(token: token.refresh_token, token_type_hint: "refresh_token")
token.expired?(leeway: 30) # => true within 30 seconds of expiring
Every request builder takes params for anything the extension you need adds to the request, such as the resource indicator of RFC 8707:
client.(
code: params[:code],
redirect_uri: "https://app.example/callback",
code_verifier: pkce.verifier,
params: {resource: "https://api.example/"}
)
These override the parameters the client sends itself, whether their keys are Strings or Symbols.
A revocation endpoint answers 200 when the token is revoked. For any other response, SimpleOAuth::OAuth2::Error.from_response(status:, body:) describes the failure.
Errors
Every error the library raises descends from SimpleOAuth::Error, so one rescue covers all of them:
begin
token = SimpleOAuth::OAuth2::Token.from_response(status: response.code, body: response.body)
rescue SimpleOAuth::Error => error
# SimpleOAuth::OAuth2::Error, SimpleOAuth::ParseError, or SimpleOAuth::InvalidOptionsError
end
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/laserlemon/simple_oauth.
This project conforms to Standard Ruby. Patches that don’t maintain that standard will not be accepted.
License
The gem is available as open source under the terms of the MIT License.