Skip to content

Commit 8fc2be2

Browse files
authored
Merge pull request #88 from Jesus/refresh-access-tokens
Refresh access tokens
2 parents b2b14d8 + 317e26a commit 8fc2be2

16 files changed

+689
-49
lines changed

README.md

Lines changed: 93 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -56,44 +56,115 @@ DropboxApi::Client.new
5656
#=> #<DropboxApi::Client ...>
5757
```
5858

59-
Note that setting an ENV variable is only a feasible choice if you're only
60-
using one account.
59+
The official documentation on the process to get an authorization code is
60+
[here](https://developers.dropbox.com/es-es/oauth-guide#implementing-oauth),
61+
it describes the two options listed below.
6162

62-
#### Option A: Get your access token from the website
6363

64-
The easiest way to obtain an access token is to get it from the Dropbox website.
65-
You just need to log in to Dropbox and refer to the *developers* section, go to
66-
*My apps* and select your application, you may need to create one if you
67-
haven't done so yet.
64+
#### Option A: Get your access token from the website
6865

69-
Under your application settings, find section *OAuth 2*. You'll find a button
70-
to generate an access token.
66+
For a quick test, you can obtain an access token from the App Console in
67+
[Dropbox's website](https://www.dropbox.com/developers/). Select from
68+
*My apps* your application, you may need to create one if you
69+
haven't done so yet. Under your application settings, find section
70+
*OAuth 2*, there is a button to generate an access token.
7171

72-
#### Option B: Use `DropboxApi::Authenticator`
72+
#### Option B: OAuth2 Code Flow
7373

74-
You can obtain an authorization code with this library:
74+
This is typically what you will use in production, you can obtain an
75+
authorization code with a 3-step process:
7576

7677
```ruby
78+
# 1. Get an authorization URL.
7779
authenticator = DropboxApi::Authenticator.new(CLIENT_ID, CLIENT_SECRET)
78-
authenticator.authorize_url #=> "https://www.dropbox.com/..."
80+
authenticator.auth_code.authorize_url #=> "https://www.dropbox.com/..."
7981

80-
# Now you need to open the authorization URL in your browser,
81-
# authorize the application and copy your code.
82+
# 2. Log into Dropbox and authorize your app. You need to open the
83+
# authorization URL in your browser.
8284

83-
auth_bearer = authenticator.get_token(CODE) #=> #<OAuth2::AccessToken ...>`
84-
auth_bearer.token #=> "VofXAX8D..."
85-
# Keep this token, you'll need it to initialize a `DropboxApi::Client` object
86-
```
85+
# 3. Exchange the authorization code for a reusable access token (not visible
86+
# to the user).
87+
access_token = authenticator.auth_code.get_token(CODE) #=> #<OAuth2::AccessToken ...>`
88+
access_token.token #=> "VofXAX8D..."
8789

88-
#### Standard OAuth 2 flow
90+
# Keep this token, you'll need it to initialize a `DropboxApi::Client` object:
91+
client = DropboxApi::Client.new(access_token: access_token)
92+
93+
# For backwards compatibility, the following also works:
94+
client = DropboxApi::Client.new(access_token.token)
95+
```
8996

90-
This is what many web applications will use. The process is described in
91-
Dropbox's [OAuth guide]
92-
(https://www.dropbox.com/developers/reference/oauth-guide#oauth-2-on-the-web).
97+
##### Integration with Rails
9398

9499
If you have a Rails application, you might be interested in this [setup
95100
guide](http://jesus.github.io/dropbox_api/file.rails_setup.html).
96101

102+
103+
##### Using refresh tokens
104+
105+
Access tokens are short-lived by default (as of September 30th, 2021),
106+
applications that require long-lived access to the API without additional
107+
interaction with the user should use refresh tokens.
108+
109+
The process is similar but a token refresh might seamlessly occur as you
110+
perform API calls. When this happens you'll need to store the
111+
new token hash if you want to continue using this session, you can use the
112+
`on_token_refreshed` callback to do this.
113+
114+
```ruby
115+
# 1. Get an authorization URL, requesting offline access type.
116+
authenticator = DropboxApi::Authenticator.new(CLIENT_ID, CLIENT_SECRET)
117+
authenticator.auth_code.authorize_url(token_access_type: 'offline')
118+
119+
# 2. Log into Dropbox and authorize your app. You need to open the
120+
# authorization URL in your browser.
121+
122+
# 3. Exchange the authorization code for a reusable access token
123+
access_token = authenticator.auth_code.get_token(CODE) #=> #<OAuth2::AccessToken ...>`
124+
125+
# You can now use the access token to initialize a DropboxApi::Client, you
126+
# should also provide a callback function to store the updated access token
127+
# whenever it's refreshed.
128+
client = DropboxApi::Client.new(
129+
access_token: access_token,
130+
on_token_refreshed: lambda { |new_token_hash|
131+
# token_hash is a serializable Hash, something like this:
132+
# {
133+
# "uid"=>"440",
134+
# "token_type"=>"bearer",
135+
# "scope"=>"account_info.read account_info.write...",
136+
# "account_id"=>"dbid:AABOLtA1rT6rRK4vajKZ...",
137+
# :access_token=>"sl.A5Ez_CBsqJILhDawHlmXSoZEhLZ4nuLFVRs6AJ...",
138+
# :refresh_token=>"iMg4Me_oKYUAAAAAAAAAAapQixCgwfXOxuubCuK_...",
139+
# :expires_at=>1632948328
140+
# }
141+
SomewhereSafe.save(new_token_hash)
142+
}
143+
)
144+
```
145+
146+
Once you've gone through the process above, you can skip the steps that require
147+
user interaction in subsequent initializations of `DropboxApi::Client`. For
148+
example:
149+
150+
```ruby
151+
# 1. Initialize an authenticator
152+
authenticator = DropboxApi::Authenticator.new(CLIENT_ID, CLIENT_SECRET)
153+
154+
# 2. Retrieve the token hash you previously stored somewhere safe, you can use
155+
# it to build a new access token.
156+
access_token = OAuth2::AccessToken.from_hash(authenticator, token_hash)
157+
158+
# 3. You now have an access token, so you can initialize a client like you
159+
# would normally:
160+
client = DropboxApi::Client.new(
161+
access_token: access_token,
162+
on_token_refreshed: lambda { |new_token_hash|
163+
SomewhereSafe.save(new_token_hash)
164+
}
165+
)
166+
```
167+
97168
### Performing API calls
98169

99170
Once you've initialized a client, for example:

lib/dropbox_api/authenticator.rb

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,11 @@
33

44
module DropboxApi
55
class Authenticator < OAuth2::Client
6-
extend Forwardable
7-
86
def initialize(client_id, client_secret)
9-
@auth_code = OAuth2::Client.new(client_id, client_secret, {
7+
super(client_id, client_secret, {
108
authorize_url: 'https://www.dropbox.com/oauth2/authorize',
119
token_url: 'https://api.dropboxapi.com/oauth2/token'
12-
}).auth_code
10+
})
1311
end
14-
15-
def_delegators :@auth_code, :authorize_url, :get_token
1612
end
1713
end

lib/dropbox_api/client.rb

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,21 @@
11
# frozen_string_literal: true
22
module DropboxApi
33
class Client
4-
def initialize(oauth_bearer = ENV['DROPBOX_OAUTH_BEARER'])
5-
@connection_builder = ConnectionBuilder.new(oauth_bearer)
4+
def initialize(
5+
oauth_bearer = ENV['DROPBOX_OAUTH_BEARER'],
6+
access_token: nil,
7+
on_token_refreshed: nil
8+
)
9+
if access_token
10+
@connection_builder = ConnectionBuilder.new(
11+
access_token: access_token,
12+
on_token_refreshed: on_token_refreshed
13+
)
14+
elsif oauth_bearer
15+
@connection_builder = ConnectionBuilder.new(oauth_bearer)
16+
else
17+
raise ArgumentError, "Either oauth_bearer or access_token should be set"
18+
end
619
end
720

821
def middleware

lib/dropbox_api/connection_builder.rb

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,51 @@ module DropboxApi
33
class ConnectionBuilder
44
attr_accessor :namespace_id
55

6-
def initialize(oauth_bearer)
7-
@oauth_bearer = oauth_bearer
6+
def initialize(oauth_bearer = nil, access_token: nil, on_token_refreshed: nil)
7+
if access_token
8+
if !access_token.is_a?(OAuth2::AccessToken)
9+
raise ArgumentError, "access_token should be an OAuth2::AccessToken"
10+
end
11+
12+
@access_token = access_token
13+
@on_token_refreshed = on_token_refreshed
14+
elsif oauth_bearer
15+
@oauth_bearer = oauth_bearer
16+
else
17+
raise ArgumentError, "Either oauth_bearer or access_token should be set"
18+
end
819
end
920

1021
def middleware
1122
@middleware ||= MiddleWare::Stack.new
1223
end
1324

25+
def can_refresh_access_token?
26+
@access_token && @access_token.refresh_token
27+
end
28+
29+
def refresh_access_token
30+
@access_token = @access_token.refresh!
31+
@on_token_refreshed.call(@access_token.to_hash) if @on_token_refreshed
32+
end
33+
34+
private def bearer
35+
@oauth_bearer or oauth_bearer_from_access_token
36+
end
37+
38+
private def oauth_bearer_from_access_token
39+
refresh_access_token if @access_token.expired?
40+
41+
@access_token.token
42+
end
43+
1444
def build(url)
1545
Faraday.new(url) do |connection|
1646
connection.use DropboxApi::MiddleWare::PathRoot, {
1747
namespace_id: self.namespace_id
1848
}
1949
middleware.apply(connection) do
20-
connection.authorization :Bearer, @oauth_bearer
21-
50+
connection.authorization :Bearer, bearer
2251
yield connection
2352
end
2453
end

lib/dropbox_api/endpoints/base.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
# frozen_string_literal: true
22
module DropboxApi::Endpoints
33
class Base
4+
def initialize(builder)
5+
@builder = builder
6+
build_connection
7+
end
8+
49
def self.add_endpoint(name, &block)
510
define_method(name, block)
611
DropboxApi::Client.add_endpoint(name, self)
@@ -10,6 +15,14 @@ def self.add_endpoint(name, &block)
1015

1116
def perform_request(params)
1217
process_response(get_response(params))
18+
rescue DropboxApi::Errors::ExpiredAccessTokenError => e
19+
if @builder.can_refresh_access_token?
20+
@builder.refresh_access_token
21+
build_connection
22+
process_response(get_response(params))
23+
else
24+
raise e
25+
end
1326
end
1427

1528
def get_response(*args)

lib/dropbox_api/endpoints/content_download.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# frozen_string_literal: true
22
module DropboxApi::Endpoints
33
class ContentDownload < DropboxApi::Endpoints::Base
4-
def initialize(builder)
5-
@connection = builder.build('https://content.dropboxapi.com') do |c|
4+
def build_connection
5+
@connection = @builder.build('https://content.dropboxapi.com') do |c|
66
c.response :decode_result
77
end
88
end

lib/dropbox_api/endpoints/content_upload.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# frozen_string_literal: true
22
module DropboxApi::Endpoints
33
class ContentUpload < DropboxApi::Endpoints::Base
4-
def initialize(builder)
5-
@connection = builder.build('https://content.dropboxapi.com') do |c|
4+
def build_connection
5+
@connection = @builder.build('https://content.dropboxapi.com') do |c|
66
c.response :decode_result
77
end
88
end

lib/dropbox_api/endpoints/rpc.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# frozen_string_literal: true
22
module DropboxApi::Endpoints
33
class Rpc < DropboxApi::Endpoints::Base
4-
def initialize(builder)
5-
@connection = builder.build('https://api.dropboxapi.com') do |c|
4+
def build_connection
5+
@connection = @builder.build('https://api.dropboxapi.com') do |c|
66
c.response :decode_result
77
end
88
end

lib/dropbox_api/endpoints/rpc_content.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# frozen_string_literal: true
22
module DropboxApi::Endpoints
33
class RpcContent < DropboxApi::Endpoints::Rpc
4-
def initialize(builder)
5-
@connection = builder.build('https://content.dropboxapi.com') do |c|
4+
def build_connection
5+
@connection = @builder.build('https://content.dropboxapi.com') do |c|
66
c.response :decode_result
77
end
88
end

lib/dropbox_api/endpoints/rpc_notify.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# frozen_string_literal: true
22
module DropboxApi::Endpoints
33
class RpcNotify < DropboxApi::Endpoints::Rpc
4-
def initialize(builder)
5-
@connection = builder.build('https://notify.dropboxapi.com') do |c|
4+
def build_connection
5+
@connection = @builder.build('https://notify.dropboxapi.com') do |c|
66
c.headers.delete 'Authorization'
77

88
c.response :decode_result

0 commit comments

Comments
 (0)