Class: SimpleOAuth::OAuth2::Client
- Inherits:
-
Object
- Object
- SimpleOAuth::OAuth2::Client
- Defined in:
- lib/simple_oauth/oauth2/client.rb,
sig/simple_oauth/oauth2.rbs
Overview
An OAuth 2.0 client that builds authorization URLs and endpoint requests
Constant Summary collapse
- AUTH_METHODS =
Client authentication methods for confidential clients (RFC 6749 Section 2.3.1)
%i[client_secret_basic client_secret_post].freeze
- FORM_CONTENT_TYPE =
The content type of every request body
"application/x-www-form-urlencoded"- EMPTY_STATE =
The error message for a state that is given but empty
"The state must not be empty"- UNPROTECTED =
The error message for an authorization request that nothing ties to its response
"Pass a pkce, or a state, so that the authorization response can be tied to this request"
Instance Attribute Summary collapse
-
#auth_method ⇒ Symbol
readonly
How a confidential client authenticates with its secret.
-
#authorization_endpoint ⇒ String?
readonly
The authorization endpoint URL.
-
#client_id ⇒ String
readonly
The client identifier.
-
#client_secret ⇒ String?
readonly
The client secret, or nil for a public client.
-
#revocation_endpoint ⇒ String?
readonly
The revocation endpoint URL.
-
#token_endpoint ⇒ String?
readonly
The token endpoint URL.
Instance Method Summary collapse
-
#authenticated(params) ⇒ Array(Hash, Hash)
private
The headers and form parameters that carry the client's credentials.
-
#authorization_code_request(code:, redirect_uri:, code_verifier: nil, params: {}) ⇒ Request
Build the request that exchanges an authorization code for a token.
-
#authorization_url(redirect_uri:, pkce:, state: nil, scope: nil, params: {}) ⇒ String
Build the URL where the user authorizes the client (RFC 6749 Section 4.1.1).
-
#basic_authorization(secret) ⇒ String
private
The HTTP Basic credentials, form-encoded first per RFC 6749 Section 2.3.1.
-
#client_credentials_request(scope: nil, params: {}) ⇒ Request
Build the request for a token that acts as the client itself.
-
#endpoint(url, name) ⇒ String
private
An endpoint URL, which must be configured.
-
#form_request(url, params, extra) ⇒ Request
private
Build an authenticated form POST.
-
#initialize(client_id:, client_secret: nil, authorization_endpoint: nil, token_endpoint: nil, revocation_endpoint: nil, auth_method: :client_secret_basic) ⇒ Client
constructor
Initialize a new client.
-
#public? ⇒ Boolean
Check whether the client is public, meaning it has no secret.
-
#refresh_token_request(refresh_token:, scope: nil, params: {}) ⇒ Request
Build the request that exchanges a refresh token for a new token.
-
#revocation_request(token:, token_type_hint: nil, params: {}) ⇒ Request
Build the request that revokes an access or refresh token (RFC 7009 Section 2.1).
-
#scope_value(scope) ⇒ String?
private
Join a list of scopes with spaces.
-
#token_request(params, extra) ⇒ Request
private
Build a request to the token endpoint.
-
#validate_protection!(pkce, state) ⇒ void
private
Checks that something ties the authorization response to the request.
Constructor Details
#initialize(client_id:, client_secret: nil, authorization_endpoint: nil, token_endpoint: nil, revocation_endpoint: nil, auth_method: :client_secret_basic) ⇒ Client
Initialize a new client
94 95 96 97 98 99 100 101 102 103 104 105 |
# File 'lib/simple_oauth/oauth2/client.rb', line 94 def initialize(client_id:, client_secret: nil, authorization_endpoint: nil, token_endpoint: nil, revocation_endpoint: nil, auth_method: :client_secret_basic) raise ArgumentError, "Unknown auth_method: #{auth_method.inspect}" unless AUTH_METHODS.include?(auth_method) @client_id = client_id @client_secret = client_secret @authorization_endpoint = @token_endpoint = token_endpoint @revocation_endpoint = revocation_endpoint @auth_method = auth_method freeze end |
Instance Attribute Details
#auth_method ⇒ Symbol (readonly)
How a confidential client authenticates with its secret
78 79 80 |
# File 'lib/simple_oauth/oauth2/client.rb', line 78 def auth_method @auth_method end |
#authorization_endpoint ⇒ String? (readonly)
The authorization endpoint URL
54 55 56 |
# File 'lib/simple_oauth/oauth2/client.rb', line 54 def @authorization_endpoint end |
#client_id ⇒ String (readonly)
The client identifier
38 39 40 |
# File 'lib/simple_oauth/oauth2/client.rb', line 38 def client_id @client_id end |
#client_secret ⇒ String? (readonly)
The client secret, or nil for a public client
46 47 48 |
# File 'lib/simple_oauth/oauth2/client.rb', line 46 def client_secret @client_secret end |
#revocation_endpoint ⇒ String? (readonly)
The revocation endpoint URL
70 71 72 |
# File 'lib/simple_oauth/oauth2/client.rb', line 70 def revocation_endpoint @revocation_endpoint end |
#token_endpoint ⇒ String? (readonly)
The token endpoint URL
62 63 64 |
# File 'lib/simple_oauth/oauth2/client.rb', line 62 def token_endpoint @token_endpoint end |
Instance Method Details
#authenticated(params) ⇒ Array(Hash, Hash)
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
The headers and form parameters that carry the client's credentials
A confidential client authenticates with HTTP Basic or in the body, and a public client identifies itself with its client_id alone (RFC 6749 Section 2.3.1).
261 262 263 264 265 266 267 268 |
# File 'lib/simple_oauth/oauth2/client.rb', line 261 def authenticated(params) headers = {"Content-Type" => FORM_CONTENT_TYPE, "Accept" => "application/json"} secret = client_secret unless public? return [headers, params.merge(client_id:)] if secret.nil? return [headers, params.merge(client_id:, client_secret: secret)] if auth_method.eql?(:client_secret_post) [headers.merge("Authorization" => (secret)), params] end |
#authorization_code_request(code:, redirect_uri:, code_verifier: nil, params: {}) ⇒ Request
Build the request that exchanges an authorization code for a token
165 166 167 |
# File 'lib/simple_oauth/oauth2/client.rb', line 165 def (code:, redirect_uri:, code_verifier: nil, params: {}) token_request({grant_type: "authorization_code", code:, redirect_uri:, code_verifier:}, params) end |
#authorization_url(redirect_uri:, pkce:, state: nil, scope: nil, params: {}) ⇒ String
Build the URL where the user authorizes the client (RFC 6749 Section 4.1.1)
The pkce is named rather than defaulted because only the caller can keep the verifier
to send with the code. OAuth 2.1 asks every client for one, so pass pkce: nil to
leave it out, for an authorization server that rejects the challenge parameters.
A state is what OAuth 2.0 ties the response to the request with. A PKCE challenge does that too, so with one the state is free to carry application state, or to be left out.
143 144 145 146 147 148 149 150 151 |
# File 'lib/simple_oauth/oauth2/client.rb', line 143 def (redirect_uri:, pkce:, state: nil, scope: nil, params: {}) validate_protection!(pkce, state) url = endpoint(, :authorization_endpoint) query = {response_type: "code", client_id:, redirect_uri:, scope: scope_value(scope), state:, code_challenge: pkce&.challenge, code_challenge_method: pkce&.challenge_method} # Symbolize the caller's keys so that a String key overrides rather than repeating a parameter query = query.merge(params.transform_keys(&:to_sym)).compact "#{url}#{url.include?("?") ? "&" : "?"}#{URI.encode_www_form(query)}" end |
#basic_authorization(secret) ⇒ String
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
The HTTP Basic credentials, form-encoded first per RFC 6749 Section 2.3.1
275 276 277 278 279 |
# File 'lib/simple_oauth/oauth2/client.rb', line 275 def (secret) credentials = [client_id, secret].map { |value| URI.encode_www_form_component(value) }.join(":") # "m0" is Base64 with no line breaks "Basic #{[credentials].pack("m0")}" end |
#client_credentials_request(scope: nil, params: {}) ⇒ Request
Build the request for a token that acts as the client itself
192 193 194 195 196 |
# File 'lib/simple_oauth/oauth2/client.rb', line 192 def client_credentials_request(scope: nil, params: {}) raise ArgumentError, "The client credentials grant requires a client secret" if public? token_request({grant_type: "client_credentials", scope: scope_value(scope)}, params) end |
#endpoint(url, name) ⇒ String
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
An endpoint URL, which must be configured
302 303 304 |
# File 'lib/simple_oauth/oauth2/client.rb', line 302 def endpoint(url, name) url || raise(ArgumentError, "The client has no #{name}") end |
#form_request(url, params, extra) ⇒ Request
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Build an authenticated form POST
The caller's own parameters are merged last, so that they can carry an extension such as the resource of RFC 8707, and can replace anything the client would send itself.
246 247 248 249 250 251 |
# File 'lib/simple_oauth/oauth2/client.rb', line 246 def form_request(url, params, extra) headers, params = authenticated(params) # Symbolize the caller's keys so that a String key overrides rather than repeating a parameter body = params.merge(extra.transform_keys(&:to_sym)).compact Request.new(method: "POST", url:, headers:, body: URI.encode_www_form(body)) end |
#public? ⇒ Boolean
Check whether the client is public, meaning it has no secret
A secret that is empty is no secret, so a client holding one cannot authenticate with it and identifies itself with its client_id alone.
116 117 118 |
# File 'lib/simple_oauth/oauth2/client.rb', line 116 def public? client_secret.to_s.empty? end |
#refresh_token_request(refresh_token:, scope: nil, params: {}) ⇒ Request
Build the request that exchanges a refresh token for a new token
179 180 181 |
# File 'lib/simple_oauth/oauth2/client.rb', line 179 def refresh_token_request(refresh_token:, scope: nil, params: {}) token_request({grant_type: "refresh_token", refresh_token:, scope: scope_value(scope)}, params) end |
#revocation_request(token:, token_type_hint: nil, params: {}) ⇒ Request
Build the request that revokes an access or refresh token (RFC 7009 Section 2.1)
208 209 210 |
# File 'lib/simple_oauth/oauth2/client.rb', line 208 def revocation_request(token:, token_type_hint: nil, params: {}) form_request(endpoint(revocation_endpoint, :revocation_endpoint), {token:, token_type_hint:}, params) end |
#scope_value(scope) ⇒ String?
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Join a list of scopes with spaces
RFC 6749 Appendix A.4 defines a scope as one or more characters, so an empty scope is omitted rather than sent as an empty parameter.
289 290 291 292 293 |
# File 'lib/simple_oauth/oauth2/client.rb', line 289 def scope_value(scope) # Array#join flattens, so a String and an Array of Strings both join correctly, and nil joins to "" value = [scope].join(" ") value unless value.empty? end |
#token_request(params, extra) ⇒ Request
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Build a request to the token endpoint
232 233 234 |
# File 'lib/simple_oauth/oauth2/client.rb', line 232 def token_request(params, extra) form_request(endpoint(token_endpoint, :token_endpoint), params, extra) end |
#validate_protection!(pkce, state) ⇒ void
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
This method returns an undefined value.
Checks that something ties the authorization response to the request
221 222 223 224 |
# File 'lib/simple_oauth/oauth2/client.rb', line 221 def validate_protection!(pkce, state) raise ArgumentError, EMPTY_STATE if !state.nil? && state.empty? raise ArgumentError, UNPROTECTED if pkce.nil? && state.nil? end |