keywright 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a5c865e5e16398211703a4fd06e282ef417ef5b95d1ff7eb7a474594b4119405
4
+ data.tar.gz: 88066856bca9240a75e7894a4c56c813ba4c647c351c12fa0d1f20b92d906d6e
5
+ SHA512:
6
+ metadata.gz: efd18518a43e444e07930913394e7f93f25a584dba3bae03f90040a40a85b4defc0cdd81f307c798d61755f7cf346c084ebff89be840e2cc8c700d865db389d6
7
+ data.tar.gz: 66739623a0fda46ab8496b3873ed65d5779b41bc917324768500f1385e33d0a1ad599456d3f8153b044ad1f2a868009e66b1edd47b34904664e6686500ec0c68
@@ -0,0 +1,61 @@
1
+ module Keywright
2
+ class Authorization
3
+
4
+ class << self
5
+
6
+ def code_from_uri( uri )
7
+ uri = URI.parse( uri )
8
+ query_string = uri.query
9
+ params = UriHelpers.parse_query( query_string )
10
+ params[ 'code' ]
11
+ end
12
+
13
+ end
14
+
15
+ def initialize( configuration = nil )
16
+ @configuration = ( configuration || Keywright.configuration )&.to_h
17
+ yield self if block_given?
18
+ end
19
+
20
+ #
21
+ # The url method will return a url, with the required parameters including the client id,
22
+ # redirect_uri, scopes and state, that the user agent should be redirected to for
23
+ # authorization.
24
+ #
25
+ # The authorization_uri and client_id are required. These must be provided through the
26
+ # options or previously assigned to the instance configuration or global configuration.
27
+ #
28
+ # The redirect_uri, scopes, and state parameters are optional. If the redirect_uri is blank
29
+ # it will not be included. If the scopes are blank the scopes from the configuration will
30
+ # be used ( if any ). If the state is blank it will not be included.
31
+ #
32
+ def url( **options )
33
+ authorization_uri = options[ :authorization_uri ] || @configuration[ :authorization_uri ]
34
+ raise ArgumentError.new( 'The authorization_uri must be provided or configured.' ) \
35
+ unless UriHelpers.valid?( authorization_uri )
36
+
37
+ client_id = options[ :client_id ] || @configuration[ :client_id ]
38
+ raise ArgumentError.new( 'The client_id must be provided or configured.' ) \
39
+ if client_id.nil?
40
+
41
+ uri = URI.parse( authorization_uri )
42
+ params = UriHelpers.parse_query( uri.query )
43
+
44
+ params = params.merge( {
45
+ 'client_id' => client_id,
46
+
47
+ 'redirect_uri' => options[ :redirect_uri ],
48
+ 'scope' => options[ :scopes ] || @configuration.scopes,
49
+ 'state' => options[ :state ]
50
+ }.compact )
51
+
52
+ uri.query = URI.encode_www_form( params )
53
+ uri.to_s
54
+ end
55
+
56
+ def code_from_uri( url )
57
+ self.class.code_from_uri( url )
58
+ end
59
+
60
+ end
61
+ end
@@ -0,0 +1,44 @@
1
+ module Keywright
2
+ class Configuration
3
+ include DynamicSchema::Definable
4
+
5
+ schema do
6
+ authorization_uri URI, required: true
7
+ token_uri URI, required: true
8
+ userinfo_uri URI
9
+
10
+ client_authorization_methods String, array: true
11
+ client_id String
12
+ client_secret String
13
+
14
+ scopes String, array: true
15
+ end
16
+
17
+ def self.build( configuration = nil, &block )
18
+ new( builder.build( configuration, &block ) )
19
+ end
20
+
21
+ def self.build!( configuration = nil, &block )
22
+ new( builder.build!( configuration, &block ) )
23
+ end
24
+
25
+ def initialize( configuration = {} )
26
+ @configuration = self.class.builder.build( configuration || {} )
27
+ end
28
+
29
+ def []( key )
30
+ @configuration[ key ]
31
+ end
32
+
33
+ def to_h
34
+ @configuration.to_h
35
+ end
36
+
37
+ end
38
+ end
39
+
40
+
41
+
42
+
43
+
44
+
@@ -0,0 +1,45 @@
1
+ module Keywright
2
+ class ErrorResult < Result
3
+
4
+ attr_reader :error, :error_description
5
+
6
+ def initialize( status_code, attributes = nil )
7
+ super( attributes )
8
+ if @error.nil? || @error_description.nil?
9
+ error, error_description = status_code_to_error( status_code )
10
+ @error ||= error
11
+ @error_description ||= error_description
12
+ end
13
+ end
14
+
15
+ private
16
+ def status_code_to_error( status_code )
17
+ case status_code
18
+ when 400
19
+ [ :invalid_request_error,
20
+ "The request format or content is invalid." ]
21
+ when 401
22
+ [ :authentication_error,
23
+ "The request credentials are invalid." ]
24
+ when 404
25
+ [ :not_found_error,
26
+ "The request resource/path was not found." ]
27
+ when 422
28
+ [ :invalid_data_error,
29
+ "The request body is invalid." ]
30
+ when 429
31
+ [ :rate_limit_error,
32
+ "The authorization service failed with a rate limit error." ]
33
+ when 500..505
34
+ [ :fatal_error,
35
+ "The authorization service failed with error: '#{status_code}'." ]
36
+ when 529
37
+ [ :overloaded_error,
38
+ "The authorization service is overloaded." ]
39
+ else
40
+ [ :unknown_error,
41
+ "The authorization service returned error: '#{status_code}'." ]
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,45 @@
1
+ module Keywright
2
+ class RefreshTokenRequest < Request
3
+
4
+ #
5
+ # The submit method makes a request to retrieve the OAuth token from the authorization server
6
+ # given the refresh_token.
7
+ #
8
+ def submit( **arguments )
9
+
10
+ refresh_token = arguments[ :refresh_token ]
11
+ raise ArgumentError.new( 'A `refresh_token` is required.' ) \
12
+ if refresh_token.nil?
13
+
14
+ token_uri = arguments[ :token_uri ] || @configuration[ :token_uri ]
15
+ raise ArgumentError.new( 'The token uri must be provided or configured.' ) \
16
+ unless UriHelpers.valid?( token_uri )
17
+
18
+ client_id = arguments[ :client_id ] || @configuration[ :client_id ]
19
+ client_secret = arguments[ :client_secret ] || @configuration[ :client_secret ]
20
+ raise ArgumentError.new( 'The client credentials must be provided or configured.' ) \
21
+ if client_id.nil? || client_secret.nil?
22
+
23
+ response = Keywright.connection.post( token_uri ) do | request |
24
+ request.headers[ 'Authorization' ] =
25
+ "Basic #{Base64.strict_encode64( "#{client_id}:#{client_secret}")}"
26
+ request.body = {
27
+ grant_type: 'refresh_token',
28
+ refresh_token: refresh_token
29
+ }
30
+ yield( request ) if block_given?
31
+ end
32
+
33
+ body = JSON.parse( response.body, symbolize_names: true ) rescue nil
34
+ result = nil
35
+ if response.success?
36
+ result = TokenResult.new( body )
37
+ else
38
+ result = ErrorResult.new( response.status, body )
39
+ end
40
+ ResponseMethods.install( response, result )
41
+
42
+ end
43
+
44
+ end
45
+ end
@@ -0,0 +1,34 @@
1
+ module Keywright
2
+
3
+ ##
4
+ # The +Request+ class encapsulates a request to the OAuth 2 server. This class serves as the
5
+ # basis for the TokenRequest and UserInfoRequest classes.
6
+ #
7
+ class Request
8
+
9
+ ##
10
+ # The +initialize+ method initializes the +Request+ instance.
11
+ #
12
+ def initialize( connection: nil, configuration: nil )
13
+ @connection = connection || Keywright.connection
14
+ @configuration = configuration || Keywright.configuration
15
+ end
16
+
17
+ protected
18
+
19
+ attr_reader :connection, :configuration
20
+
21
+ def post( uri, body = {}, headers = {}, &block )
22
+ @connection.post( uri, body, headers, &block )
23
+ end
24
+
25
+ def get( uri, headers = nil, &block )
26
+ @connection.get( uri ) do | request |
27
+ headers&.each { | key, value | request.headers[ key ] = value }
28
+ block.call( request ) if block
29
+ end
30
+ end
31
+
32
+ end
33
+
34
+ end
@@ -0,0 +1,15 @@
1
+ module Keywright
2
+ #
3
+ # The ResponseMethods module extends a Faraday reponse, adding the +result+ method.
4
+ #
5
+ module ResponseMethods
6
+ def self.install( response, result )
7
+ response.instance_variable_set( "@_keywright_result", result )
8
+ response.extend( ResponseMethods )
9
+ end
10
+
11
+ def result
12
+ @_keywright_result
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,9 @@
1
+ module Keywright
2
+ class Result
3
+ def initialize( attributes = {} )
4
+ attributes&.each do | key, value |
5
+ self.instance_variable_set( "@#{key}", value )
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,46 @@
1
+ module Keywright
2
+ class TokenRequest < Request
3
+
4
+ #
5
+ # The submit method makes a request to retrieve the OAuth token from the authorization server
6
+ # given the authorization code.
7
+ #
8
+ def submit( **arguments )
9
+
10
+ code = arguments[ :code ]
11
+ raise ArgumentError.new( 'An authorization `code` is required.' ) \
12
+ if code.nil?
13
+
14
+ token_uri = arguments[ :token_uri ] || @configuration[ :token_uri ]
15
+ raise ArgumentError.new( 'The token uri must be provided or configured.' ) \
16
+ unless UriHelpers.valid?( token_uri )
17
+
18
+ client_id = arguments[ :client_id ] || @configuration[ :client_id ]
19
+ client_secret = arguments[ :client_secret ] || @configuration[ :client_secret ]
20
+ raise ArgumentError.new( 'The client credentials must be configured.' ) \
21
+ if client_id.nil? || client_secret.nil?
22
+
23
+ response = Keywright.connection.post( token_uri ) do | request |
24
+ request.headers[ 'Authorization' ] =
25
+ "Basic #{Base64.strict_encode64( "#{client_id}:#{client_secret}")}"
26
+ request.body = {
27
+ grant_type: 'authorization_code',
28
+ code: code,
29
+ redirect_uri: arguments[ :redirect_uri ]
30
+ }
31
+ yield( request ) if block_given?
32
+ end
33
+
34
+ body = JSON.parse( response.body, symbolize_names: true ) rescue nil
35
+ result = nil
36
+ if response.success?
37
+ result = TokenResult.new( body )
38
+ else
39
+ result = ErrorResult.new( response.status, body )
40
+ end
41
+ ResponseMethods.install( response, result )
42
+
43
+ end
44
+
45
+ end
46
+ end
@@ -0,0 +1,5 @@
1
+ module Keywright
2
+ class TokenResult < Result
3
+ attr_accessor :access_token, :token_type, :expires_in, :refresh_token
4
+ end
5
+ end
@@ -0,0 +1,45 @@
1
+ module Keywright
2
+ module UriHelpers
3
+
4
+ def valid?( uri )
5
+ uri = uri.is_a?( URI ) ? uri : URI.parse( uri.to_s )
6
+ return false unless uri.is_a?( URI::HTTP ) || uri.is_a?( URI::HTTPS )
7
+ return false if uri.host.nil? || uri.host.strip.empty?
8
+ true
9
+ rescue URI::InvalidURIError
10
+ false
11
+ end
12
+
13
+ def parse_query( query, delimiter = nil )
14
+ params = {}
15
+
16
+ max_key_space = 1024
17
+ bytes = 0
18
+
19
+ ( query || '' ).split( delimiter ? /[#{delimiter}] */n : /[&;] */n ).each do | pair |
20
+ key, value = pair.split( '=', 2 ).map { | x | URI.decode_www_form_component( x ) }
21
+
22
+ if key
23
+ bytes += key.size
24
+ if bytes > max_key_space
25
+ raise RangeError, "exceeded available parameter key space"
26
+ end
27
+ end
28
+
29
+ if current = params[ key ]
30
+ if current.class == Array
31
+ params[ key ] << value
32
+ else
33
+ params[ key ] = [ current, value ]
34
+ end
35
+ else
36
+ params[ key ] = value
37
+ end
38
+ end
39
+
40
+ return params
41
+ end
42
+
43
+ module_function :valid?, :parse_query
44
+ end
45
+ end
@@ -0,0 +1,33 @@
1
+ module Keywright
2
+ class UserInfoRequest < Request
3
+
4
+ #
5
+ # The submit method makes a request to retrieve the OIDC userinfo given an access token.
6
+ #
7
+ def submit( **arguments )
8
+
9
+ access_token = arguments[ :access_token ]
10
+ raise ArgumentError.new( 'An access token is required.' ) if access_token.nil?
11
+
12
+ userinfo_uri = arguments[ :userinfo_uri ] || @configuration[ :userinfo_uri ]
13
+ raise ArgumentError.new( 'The userinfo uri must be provided or configured.' ) \
14
+ unless UriHelpers.valid?( userinfo_uri )
15
+
16
+ response = Keywright.connection.get( userinfo_uri ) do | request |
17
+ request.headers[ 'Authorization' ] = "Bearer #{access_token}"
18
+ yield( request ) if block_given?
19
+ end
20
+
21
+ body = JSON.parse( response.body, symbolize_names: true ) rescue nil
22
+ result = nil
23
+ if response.success?
24
+ result = UserInfoResult.new( body )
25
+ else
26
+ result = ErrorResult.new( response.status, body )
27
+ end
28
+ ResponseMethods.install( response, result )
29
+
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,23 @@
1
+ module Keywright
2
+ class UserInfoResult < Result
3
+ attr_accessor :updated_at
4
+ attr_accessor :sub
5
+ attr_accessor :name
6
+ attr_accessor :given_name
7
+ attr_accessor :family_name
8
+ attr_accessor :middle_name
9
+ attr_accessor :nickname
10
+ attr_accessor :preferred_username
11
+ attr_accessor :profile
12
+ attr_accessor :picture
13
+ attr_accessor :website
14
+ attr_accessor :email
15
+ attr_accessor :email_verified
16
+ attr_accessor :gender
17
+ attr_accessor :birthdate
18
+ attr_accessor :zoneinfo
19
+ attr_accessor :locale
20
+ attr_accessor :phone_number
21
+ attr_accessor :phone_number_verified
22
+ end
23
+ end
@@ -0,0 +1,3 @@
1
+ module Keywright
2
+ VERSION = "0.0.1"
3
+ end
data/lib/keywright.rb ADDED
@@ -0,0 +1,57 @@
1
+ require 'uri'
2
+ require 'base64'
3
+ require 'faraday'
4
+ require 'dynamic_schema'
5
+
6
+ require 'keywright/version'
7
+ require 'keywright/uri_helpers'
8
+ require 'keywright/configuration'
9
+ require 'keywright/authorization'
10
+ require 'keywright/result'
11
+ require 'keywright/error_result'
12
+ require 'keywright/token_result'
13
+ require 'keywright/response_methods'
14
+ require 'keywright/request'
15
+ require 'keywright/token_request'
16
+ require 'keywright/refresh_token_request'
17
+ require 'keywright/user_info_result'
18
+ require 'keywright/user_info_request'
19
+ require 'keywright'
20
+
21
+ module Keywright
22
+ class << self
23
+
24
+ #
25
+ # The connection method allows the caller to setup the Farday connection used
26
+ # by Keywright in interacting with the authorization server.
27
+ #
28
+ def connection
29
+ @connection ||= Faraday.new do | connection |
30
+ connection.request :url_encoded
31
+ connection.adapter Faraday.default_adapter
32
+ end
33
+ yield( @connection ) if block_given?
34
+ @connection
35
+ end
36
+
37
+ # the default singleton keywright configuration may be assigned; it will used when a when
38
+ # neither a required parameter or configuration are otherwise provided
39
+ attr_writer :configuration
40
+
41
+ #
42
+ # The configuration method returns the default configuration and, if the caller supplies
43
+ # a block, allows that caller to customize this configuration.
44
+ #
45
+ # The configuration includes the client credentials, the urls of the authorization,
46
+ # token and userinfo endpoints, the grant types, scopes, and authentication methods
47
+ # the authorization server supports for the token endpoint.
48
+ #
49
+ # This method may be called repeatedly to update the global configuration.
50
+ #
51
+ def configuration( &block )
52
+ @configuration = Configuration.build!( @configuration&.to_h, &block ) if block_given?
53
+ @configuration
54
+ end
55
+
56
+ end
57
+ end
metadata ADDED
@@ -0,0 +1,124 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: keywright
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Kristoph Cichocki-Romanov
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 2025-01-14 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: faraday
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.7'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.7'
26
+ - !ruby/object:Gem::Dependency
27
+ name: dynamicschema
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: 1.0.0.beta04
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: 1.0.0.beta04
40
+ - !ruby/object:Gem::Dependency
41
+ name: rspec
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.13'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.13'
54
+ - !ruby/object:Gem::Dependency
55
+ name: debug
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.9'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.9'
68
+ - !ruby/object:Gem::Dependency
69
+ name: vcr
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '6.3'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '6.3'
82
+ description: 'The Keywright gem implements a lightweigh, flexible client for use with '
83
+ email:
84
+ - rubygems.org@kristoph.net
85
+ executables: []
86
+ extensions: []
87
+ extra_rdoc_files: []
88
+ files:
89
+ - lib/keywright.rb
90
+ - lib/keywright/authorization.rb
91
+ - lib/keywright/configuration.rb
92
+ - lib/keywright/error_result.rb
93
+ - lib/keywright/refresh_token_request.rb
94
+ - lib/keywright/request.rb
95
+ - lib/keywright/response_methods.rb
96
+ - lib/keywright/result.rb
97
+ - lib/keywright/token_request.rb
98
+ - lib/keywright/token_result.rb
99
+ - lib/keywright/uri_helpers.rb
100
+ - lib/keywright/user_info_request.rb
101
+ - lib/keywright/user_info_result.rb
102
+ - lib/keywright/version.rb
103
+ homepage: https://github.com/EndlessInternational/keywright
104
+ licenses:
105
+ - MIT
106
+ metadata: {}
107
+ rdoc_options: []
108
+ require_paths:
109
+ - lib
110
+ required_ruby_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: '3.0'
115
+ required_rubygems_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ requirements: []
121
+ rubygems_version: 3.6.2
122
+ specification_version: 4
123
+ summary: A flexible and lightweignt OAuth 2 Authorization / OIDC client.
124
+ test_files: []