From 48a6b789344ab2aeeebafe7a677975fc4202dba7 Mon Sep 17 00:00:00 2001 From: Neha Bajaj Date: Fri, 1 Sep 2023 14:24:23 +0530 Subject: [PATCH] feat: Expose Granted Scopes while fetching credentials (#230) --- lib/signet/oauth_2/client.rb | 127 +++++++++++++++++++---------- spec/signet/oauth_2/client_spec.rb | 14 +++- 2 files changed, 95 insertions(+), 46 deletions(-) diff --git a/lib/signet/oauth_2/client.rb b/lib/signet/oauth_2/client.rb index a498c42..80aa0eb 100644 --- a/lib/signet/oauth_2/client.rb +++ b/lib/signet/oauth_2/client.rb @@ -32,52 +32,54 @@ class Client # # @param [Hash] options # The configuration parameters for the client. - # - :authorization_uri - + # - `:authorization_uri` - # The authorization server's HTTP endpoint capable of # authenticating the end-user and obtaining authorization. - # - :token_credential_uri - + # - `:token_credential_uri` - # The authorization server's HTTP endpoint capable of issuing # tokens and refreshing expired tokens. - # - :client_id - + # - `:client_id` - # A unique identifier issued to the client to identify itself to the # authorization server. - # - :client_secret - + # - `:client_secret` - # A shared symmetric secret issued by the authorization server, # which is used to authenticate the client. - # - :scope - + # - `:scope` - # The scope of the access request, expressed either as an Array # or as a space-delimited String. - # - :target_audience - + # - `:target_audience` - # The final target audience for ID tokens fetched by this client, # as a String. - # - :state - + # - `:state` - # An arbitrary string designed to allow the client to maintain state. - # - :code - + # - `:code` - # The authorization code received from the authorization server. - # - :redirect_uri - + # - `:redirect_uri` - # The redirection URI used in the initial request. - # - :username - + # - `:username` - # The resource owner's username. - # - :password - + # - `:password` - # The resource owner's password. - # - :issuer - + # - `:issuer` - # Issuer ID when using assertion profile - # - :person - + # - `:person` - # Target user for assertions - # - :expiry - + # - `:expiry` - # Number of seconds assertions are valid for - # - :signing_key - + # - `:signing_key` - # Signing key when using assertion profile - # - :refresh_token - + # - `:refresh_token` - # The refresh token associated with the access token # to be refreshed. - # - :access_token - + # - `:access_token` - # The current access token for this client. - # - :id_token - + # - `:id_token` - # The current ID token for this client. - # - :extension_parameters - + # - `:extension_parameters` - # When using an extension grant type, this the set of parameters used # by that extension. + # - `:granted_scopes` - + # All scopes granted by authorization server. # # @example # client = Signet::OAuth2::Client.new( @@ -109,6 +111,7 @@ def initialize options = {} @state = nil @username = nil @access_type = nil + @granted_scopes = nil update! options end @@ -117,56 +120,58 @@ def initialize options = {} # # @param [Hash] options # The configuration parameters for the client. - # - :authorization_uri - + # - `:authorization_uri` - # The authorization server's HTTP endpoint capable of # authenticating the end-user and obtaining authorization. - # - :token_credential_uri - + # - `:token_credential_uri` - # The authorization server's HTTP endpoint capable of issuing # tokens and refreshing expired tokens. - # - :client_id - + # - `:client_id` - # A unique identifier issued to the client to identify itself to the # authorization server. - # - :client_secret - + # - `:client_secret` - # A shared symmetric secret issued by the authorization server, # which is used to authenticate the client. - # - :scope - + # - `:scope` - # The scope of the access request, expressed either as an Array # or as a space-delimited String. - # - :target_audience - + # - `:target_audience` - # The final target audience for ID tokens fetched by this client, # as a String. - # - :state - + # - `:state` - # An arbitrary string designed to allow the client to maintain state. - # - :code - + # - `:code` - # The authorization code received from the authorization server. - # - :redirect_uri - + # - `:redirect_uri` - # The redirection URI used in the initial request. - # - :username - + # - `:username` - # The resource owner's username. - # - :password - + # - `:password` - # The resource owner's password. - # - :issuer - + # - `:issuer` - # Issuer ID when using assertion profile - # - :audience - + # - `:audience` - # Target audience for assertions - # - :person - + # - `:person` - # Target user for assertions - # - :expiry - + # - `:expiry` - # Number of seconds assertions are valid for - # - :signing_key - + # - `:signing_key` - # Signing key when using assertion profile - # - :refresh_token - + # - `:refresh_token` - # The refresh token associated with the access token # to be refreshed. - # - :access_token - + # - `:access_token` - # The current access token for this client. - # - :access_type - + # - `:access_type` - # The current access type parameter for #authorization_uri. - # - :id_token - + # - `:id_token` - # The current ID token for this client. - # - :extension_parameters - + # - `:extension_parameters` - # When using an extension grant type, this is the set of parameters used # by that extension. + # - `:granted_scopes` - + # All scopes granted by authorization server. # # @example # client.update!( @@ -253,7 +258,7 @@ def update_token! options = {} self.access_token = options[:access_token] if options.key? :access_token self.refresh_token = options[:refresh_token] if options.key? :refresh_token self.id_token = options[:id_token] if options.key? :id_token - + self.granted_scopes = options[:granted_scopes] if options.key? :granted_scopes self end @@ -823,6 +828,33 @@ def expires_at= new_expires_at @expires_at = normalize_timestamp new_expires_at end + ## + # Returns the scopes granted by the authorization server. + # + # @return [Array, nil] The scope of access returned by the authorization server. + def granted_scopes + @granted_scopes + end + + ## + # Sets the scopes returned by authorization server for this client. + # + # @param [String, Array, nil] new_granted_scopes + # The scope of access returned by authorization server. This will + # ideally be expressed as space-delimited String. + def granted_scopes= new_granted_scopes + case new_granted_scopes + when Array + @granted_scopes = new_granted_scopes + when String + @granted_scopes = new_granted_scopes.split + when nil + @granted_scopes = nil + else + raise TypeError, "Expected Array or String, got #{new_granted_scopes.class}" + end + end + ## # Returns true if the access token has expired. # Returns false if the token has not expired or has an nil @expires_at. @@ -857,6 +889,7 @@ def clear_credentials! @code = nil @issued_at = nil @expires_at = nil + @granted_scopes = nil end ## @@ -936,7 +969,8 @@ def to_json *_args "refresh_token" => refresh_token, "access_token" => access_token, "id_token" => id_token, - "extension_parameters" => extension_parameters + "extension_parameters" => extension_parameters, + "granted_scopes" => granted_scopes ) end @@ -1020,19 +1054,22 @@ def fetch_access_token options = {} content_type = response.header[:content_type] end - return ::Signet::OAuth2.parse_credentials body, content_type if status == 200 - message = " Server message:\n#{response.body.to_s.strip}" unless body.to_s.strip.empty? + if [400, 401, 403].include? status message = "Authorization failed.#{message}" raise ::Signet::AuthorizationError.new message, response: response elsif status.to_s[0] == "5" message = "Remote server error.#{message}" raise ::Signet::RemoteServerError, message - else + elsif status != 200 message = "Unexpected status code: #{response.status}.#{message}" raise ::Signet::UnexpectedStatusError, message end + # status == 200 + parsed_response = ::Signet::OAuth2.parse_credentials body, content_type + parsed_response["granted_scopes"] = parsed_response.delete("scope") if parsed_response + parsed_response end def fetch_access_token! options = {} diff --git a/spec/signet/oauth_2/client_spec.rb b/spec/signet/oauth_2/client_spec.rb index 484030a..505278a 100644 --- a/spec/signet/oauth_2/client_spec.rb +++ b/spec/signet/oauth_2/client_spec.rb @@ -63,6 +63,16 @@ def build_form_encoded_response payload expect(@client.scope).to eq ["legit", "alsolegit"] end + it "should allow to set granted scopes as String" do + @client.granted_scopes = "granted_scopes1 granted_scopes2" + expect(@client.granted_scopes).to eq ["granted_scopes1", "granted_scopes2"] + end + + it "should allow to set granted scopes as Array" do + @client.granted_scopes = ["granted_scopes1", "granted_scopes2"] + expect(@client.granted_scopes).to eq ["granted_scopes1", "granted_scopes2"] + end + it "should raise an error if a bogus redirect URI is provided" do expect(lambda do @client = Signet::OAuth2::Client.new redirect_uri: :bogus @@ -375,13 +385,15 @@ def build_form_encoded_response payload :access_token => "12345", refresh_token: "54321", :expires_in => 3600, - :issued_at => issued_at + :issued_at => issued_at, + :granted_scopes => "scope1" ) expect(@client.access_token).to eq "12345" expect(@client.refresh_token).to eq "54321" expect(@client.expires_in).to eq 3600 expect(@client.issued_at).to eq issued_at expect(@client).to_not be_expired + expect(@client.granted_scopes).to eq ["scope1"] end it "should handle expires as equivalent to expires_in" do