Class: Manceps::Auth::OAuth
- Inherits:
-
Object
- Object
- Manceps::Auth::OAuth
- Defined in:
- lib/manceps/auth/oauth.rb
Overview
OAuth 2.1 authentication with discovery, PKCE, and token refresh.
Defined Under Namespace
Classes: Discovery
Instance Attribute Summary collapse
-
#access_token ⇒ Object
readonly
Returns the value of attribute access_token.
-
#expires_at ⇒ Object
readonly
Returns the value of attribute expires_at.
-
#refresh_token ⇒ Object
readonly
Returns the value of attribute refresh_token.
Class Method Summary collapse
-
.authorize_url(authorization_url:, client_id:, redirect_uri:, state:, scopes: nil, code_challenge: nil) ⇒ Object
Build authorization URL for user redirect.
-
.discover(server_url, redirect_uri:, client_name: 'Manceps') ⇒ Object
Fetch OAuth Authorization Server Metadata (RFC 8414) and optionally perform Dynamic Client Registration (RFC 7591).
-
.exchange_code(token_url:, client_id:, code:, redirect_uri:, client_secret: nil, code_verifier: nil) ⇒ Object
Exchange authorization code for tokens.
- .fetch_json(response, context) ⇒ Object
-
.generate_pkce ⇒ Object
PKCE helpers (RFC 7636).
- .register_client(http, discovery, redirect_uri, client_name) ⇒ Object
Instance Method Summary collapse
- #apply(headers) ⇒ Object
-
#initialize(access_token:, refresh_token: nil, token_url: nil, client_id: nil, client_secret: nil, expires_at: nil, on_token_refresh: nil) ⇒ OAuth
constructor
A new instance of OAuth.
Constructor Details
#initialize(access_token:, refresh_token: nil, token_url: nil, client_id: nil, client_secret: nil, expires_at: nil, on_token_refresh: nil) ⇒ OAuth
Returns a new instance of OAuth.
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
# File 'lib/manceps/auth/oauth.rb', line 25 def initialize( access_token:, refresh_token: nil, token_url: nil, client_id: nil, client_secret: nil, expires_at: nil, on_token_refresh: nil ) @access_token = access_token @refresh_token = refresh_token @token_url = token_url @client_id = client_id @client_secret = client_secret @expires_at = expires_at @on_token_refresh = on_token_refresh @mutex = Mutex.new end |
Instance Attribute Details
#access_token ⇒ Object (readonly)
Returns the value of attribute access_token.
23 24 25 |
# File 'lib/manceps/auth/oauth.rb', line 23 def access_token @access_token end |
#expires_at ⇒ Object (readonly)
Returns the value of attribute expires_at.
23 24 25 |
# File 'lib/manceps/auth/oauth.rb', line 23 def expires_at @expires_at end |
#refresh_token ⇒ Object (readonly)
Returns the value of attribute refresh_token.
23 24 25 |
# File 'lib/manceps/auth/oauth.rb', line 23 def refresh_token @refresh_token end |
Class Method Details
.authorize_url(authorization_url:, client_id:, redirect_uri:, state:, scopes: nil, code_challenge: nil) ⇒ Object
Build authorization URL for user redirect
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 |
# File 'lib/manceps/auth/oauth.rb', line 109 def self.(authorization_url:, client_id:, redirect_uri:, state:, scopes: nil, code_challenge: nil) params = { 'response_type' => 'code', 'client_id' => client_id, 'redirect_uri' => redirect_uri, 'state' => state } params['scope'] = Array(scopes).join(' ') if !scopes.nil? && !Array(scopes).empty? if code_challenge params['code_challenge'] = code_challenge params['code_challenge_method'] = 'S256' end "#{}?#{URI.encode_www_form(params)}" end |
.discover(server_url, redirect_uri:, client_name: 'Manceps') ⇒ Object
Fetch OAuth Authorization Server Metadata (RFC 8414) and optionally perform Dynamic Client Registration (RFC 7591).
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
# File 'lib/manceps/auth/oauth.rb', line 51 def self.discover(server_url, redirect_uri:, client_name: 'Manceps') server_uri = URI.parse(server_url) port_suffix = [80, 443].include?(server_uri.port) ? '' : ":#{server_uri.port}" well_known = "#{server_uri.scheme}://#{server_uri.host}#{port_suffix}/.well-known/oauth-authorization-server" http = HTTPX.with(timeout: { connect_timeout: 10, request_timeout: 30 }) = fetch_json(http.get(well_known), 'OAuth discovery') discovery = Discovery.new( authorization_url: ['authorization_endpoint'], token_url: ['token_endpoint'], registration_endpoint: ['registration_endpoint'], scopes: ['scopes_supported'] ) register_client(http, discovery, redirect_uri, client_name) discovery end |
.exchange_code(token_url:, client_id:, code:, redirect_uri:, client_secret: nil, code_verifier: nil) ⇒ Object
Exchange authorization code for tokens
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 |
# File 'lib/manceps/auth/oauth.rb', line 126 def self.exchange_code(token_url:, client_id:, code:, redirect_uri:, client_secret: nil, code_verifier: nil) body = { 'grant_type' => 'authorization_code', 'code' => code, 'redirect_uri' => redirect_uri, 'client_id' => client_id } body['client_secret'] = client_secret if !client_secret.nil? && !client_secret.empty? body['code_verifier'] = code_verifier if !code_verifier.nil? && !code_verifier.empty? http = HTTPX.with(timeout: { connect_timeout: 10, request_timeout: 30 }) response = http.post( token_url, headers: { 'content-type' => 'application/x-www-form-urlencoded' }, body: URI.encode_www_form(body) ) data = fetch_json(response, 'Token exchange') unless data['access_token'] raise Manceps::AuthenticationError, "Token exchange failed: #{data['error_description'] || data['error'] || 'no access_token'}" end data end |
.fetch_json(response, context) ⇒ Object
96 97 98 99 100 101 102 103 104 105 106 |
# File 'lib/manceps/auth/oauth.rb', line 96 def self.fetch_json(response, context) if response.status >= 400 raise Manceps::AuthenticationError, "#{context} failed (HTTP #{response.status})" end JSON.parse(response.body.to_s) rescue JSON::ParserError raise Manceps::AuthenticationError, "#{context}: invalid response (not JSON): #{response.body.to_s[0..200]}" end |
.generate_pkce ⇒ Object
PKCE helpers (RFC 7636)
153 154 155 156 157 158 159 |
# File 'lib/manceps/auth/oauth.rb', line 153 def self.generate_pkce verifier = SecureRandom.urlsafe_base64(32) challenge = Base64.urlsafe_encode64( OpenSSL::Digest::SHA256.digest(verifier), padding: false ) { verifier: verifier, challenge: challenge } end |
.register_client(http, discovery, redirect_uri, client_name) ⇒ Object
70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
# File 'lib/manceps/auth/oauth.rb', line 70 def self.register_client(http, discovery, redirect_uri, client_name) reg_endpoint = discovery.registration_endpoint return if reg_endpoint.nil? || reg_endpoint.empty? reg_response = http.post( reg_endpoint, headers: { 'content-type' => 'application/json' }, body: JSON.generate({ client_name: client_name, redirect_uris: [redirect_uri], grant_types: %w[authorization_code refresh_token], response_types: ['code'], token_endpoint_auth_method: 'client_secret_post' }) ) reg_data = fetch_json(reg_response, 'Client registration') unless reg_data['client_id'] raise Manceps::AuthenticationError, "Client registration failed: #{reg_data['error']}" end discovery.client_id = reg_data['client_id'] discovery.client_secret = reg_data['client_secret'] end |
Instance Method Details
#apply(headers) ⇒ Object
44 45 46 47 |
# File 'lib/manceps/auth/oauth.rb', line 44 def apply(headers) refresh_if_needed! headers['authorization'] = "Bearer #{@access_token}" end |