keywright 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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: []