Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Expose Granted Scopes while fetching credentials #230

Merged
merged 6 commits into from
Sep 1, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 34 additions & 5 deletions lib/signet/oauth_2/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ class Client
# - <code>:extension_parameters</code> -
# When using an extension grant type, this the set of parameters used
# by that extension.
# - <code>:granted_scopes</code> -
# All scopes granted by authorization server.
#
# @example
# client = Signet::OAuth2::Client.new(
Expand Down Expand Up @@ -109,6 +111,7 @@ def initialize options = {}
@state = nil
@username = nil
@access_type = nil
@granted_scopes = nil
bajajneha27 marked this conversation as resolved.
Show resolved Hide resolved
update! options
end

Expand Down Expand Up @@ -167,6 +170,8 @@ def initialize options = {}
# - <code>:extension_parameters</code> -
# When using an extension grant type, this is the set of parameters used
# by that extension.
# - <code>:granted_scopes</code> -
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use backticks instead of html tags?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

# All scopes granted by authorization server.
#
# @example
# client.update!(
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -823,6 +828,25 @@ def expires_at= new_expires_at
@expires_at = normalize_timestamp new_expires_at
end

##
# Returns the scopes granted by the authorization server.
#
# @return [Array] The scope of access returned by the authorization server.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this can return nil as well as Array, if the granted scopes were never set.

def granted_scopes
@granted_scopes
end

##
# Sets the scopes returned by authorization server for this client.
#
# @param [Array] new_granted_scopes
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want to support both Array and String (see your docs below), you should include both types here.

Also, do we need to support setting it back to nil?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing this out. This was a miss from my end.
The auth server returns a space delimited string, so I thought of supporting only String for now.
And yes, we can set it back to nil.

# The scope of access the client is requesting. This may be
# expressed as either an Array of String objects or as a
# space-delimited String.
def granted_scopes= new_granted_scopes
@granted_scopes = new_granted_scopes&.split
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If an Array is passed in for new_granted_scopes (see your docs above), it won't respond to the message split.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. I have updated the docs to reflect the expected behavior here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest allowing Array to be passed. Otherwise, granted_scopes= takes a String but granted_scopes returns an Array, which would be confusing and invite buggy usage.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't want this method to accept Array since we get space delimited String in response and this method should ideally not be used to update granted_scopes manually.
But for the sake of not introducing bugs, I'll make the change.

end

##
# Returns true if the access token has expired.
# Returns false if the token has not expired or has an nil @expires_at.
Expand Down Expand Up @@ -857,6 +881,7 @@ def clear_credentials!
@code = nil
@issued_at = nil
@expires_at = nil
@granted_scopes = nil
end

##
Expand Down Expand Up @@ -936,7 +961,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

Expand Down Expand Up @@ -1020,19 +1046,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 = {}
Expand Down
9 changes: 8 additions & 1 deletion spec/signet/oauth_2/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ def build_form_encoded_response payload
expect(@client.scope).to eq ["legit", "alsolegit"]
end

it "should allow to set granted scopes" do
@client.granted_scopes = "granted_scopes1 granted_scopes2"
expect(@client.granted_scopes).to eq ["granted_scopes1", "granted_scopes2"]
end
bajajneha27 marked this conversation as resolved.
Show resolved Hide resolved

it "should raise an error if a bogus redirect URI is provided" do
expect(lambda do
@client = Signet::OAuth2::Client.new redirect_uri: :bogus
Expand Down Expand Up @@ -375,13 +380,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
Expand Down