7
7
using System ;
8
8
using System . Linq ;
9
9
using System . Threading . Tasks ;
10
+ using System . Threading ; // Interlocked
11
+
12
+ // https://support.box.com/hc/en-us/community/posts/360049144934-Refresh-Token-Expiring-in-1-hour
10
13
11
14
namespace ClassTranscribeDatabase . Services
12
15
{
13
16
public class BoxAPI
14
17
{
18
+ private static int refreshing = 0 ;
15
19
private readonly SlackLogger _slack ;
16
20
private readonly ILogger _logger ;
21
+ private readonly BoxConfig _config ;
17
22
public BoxAPI ( ILogger < BoxAPI > logger , SlackLogger slack )
18
23
{
19
24
_logger = logger ;
20
25
_slack = slack ;
26
+ _config = new BoxConfig ( Globals . appSettings . BOX_CLIENT_ID , Globals . appSettings . BOX_CLIENT_SECRET , new Uri ( "http://locahost" ) ) ;
21
27
}
22
28
29
+ // Used by Controller/BoxController.cs
23
30
// To generate authCode on a browser open,
24
31
// https://account.box.com/api/oauth2/authorize?client_id=[CLIENT_ID]&response_type=code
25
32
/// <summary>Updates Box accessToken and refreshToken values in the Dictionary table.
@@ -30,30 +37,11 @@ public async Task CreateAccessTokenAsync(string authCode)
30
37
// This implementation is overly chatty with the database, but we rarely create access tokens so it is not a problem
31
38
using ( var _context = CTDbContext . CreateDbContext ( ) )
32
39
{
33
- if ( ! await _context . Dictionaries . Where ( d => d . Key == CommonUtils . BOX_ACCESS_TOKEN ) . AnyAsync ( ) )
34
- {
35
- _context . Dictionaries . Add ( new Dictionary
36
- {
37
- Key = CommonUtils . BOX_ACCESS_TOKEN
38
- } ) ;
39
- await _context . SaveChangesAsync ( ) ;
40
- }
41
- if ( ! await _context . Dictionaries . Where ( d => d . Key == CommonUtils . BOX_REFRESH_TOKEN ) . AnyAsync ( ) )
42
- {
43
- _context . Dictionaries . Add ( new Dictionary
44
- {
45
- Key = CommonUtils . BOX_REFRESH_TOKEN
46
- } ) ;
47
- await _context . SaveChangesAsync ( ) ;
48
- }
49
-
50
-
51
- var accessToken = _context . Dictionaries . Where ( d => d . Key == CommonUtils . BOX_ACCESS_TOKEN ) . First ( ) ;
52
- var refreshToken = _context . Dictionaries . Where ( d => d . Key == CommonUtils . BOX_REFRESH_TOKEN ) . First ( ) ;
53
- var config = new BoxConfig ( Globals . appSettings . BOX_CLIENT_ID , Globals . appSettings . BOX_CLIENT_SECRET , new Uri ( "http://locahost" ) ) ;
54
- var client = new Box . V2 . BoxClient ( config ) ;
40
+ var client = new Box . V2 . BoxClient ( _config ) ;
55
41
var auth = await client . Auth . AuthenticateAsync ( authCode ) ;
56
42
_logger . LogInformation ( "Created Box Tokens" ) ;
43
+ //Dictionary accessToken,refreshToken;
44
+ var ( accessToken , refreshToken ) = await getOrCreateDatabaseEntries ( _context ) ;
57
45
accessToken . Value = auth . AccessToken ;
58
46
refreshToken . Value = auth . RefreshToken ;
59
47
await _context . SaveChangesAsync ( ) ;
@@ -64,23 +52,42 @@ public async Task CreateAccessTokenAsync(string authCode)
64
52
/// </summary>
65
53
public async Task RefreshAccessTokenAsync ( )
66
54
{
55
+ _logger . LogInformation ( "RefreshAccessTokenAsync: Starting" ) ;
67
56
try
68
57
{
69
58
using ( var _context = CTDbContext . CreateDbContext ( ) )
70
59
{
71
- var accessToken = await _context . Dictionaries . Where ( d => d . Key == CommonUtils . BOX_ACCESS_TOKEN ) . FirstAsync ( ) ;
72
- var refreshToken = await _context . Dictionaries . Where ( d => d . Key == CommonUtils . BOX_REFRESH_TOKEN ) . FirstAsync ( ) ;
73
- var config = new BoxConfig ( Globals . appSettings . BOX_CLIENT_ID , Globals . appSettings . BOX_CLIENT_SECRET , new Uri ( "http://locahost" ) ) ;
60
+ // Dictionary accessToken,refreshToken ;
61
+ var ( accessToken , refreshToken ) = await getOrCreateDatabaseEntries ( _context ) ;
62
+
74
63
var auth = new OAuthSession ( accessToken . Value , refreshToken . Value , 3600 , "bearer" ) ;
75
- var client = new BoxClient ( config , auth ) ;
64
+ var client = new BoxClient ( _config , auth ) ;
76
65
/// Try to refresh the access token
77
66
auth = await client . Auth . RefreshAccessTokenAsync ( auth . AccessToken ) ;
67
+ _logger . LogInformation ( "RefreshAccessTokenAsync: Complete (RefreshAccessTokenAsync returned)" ) ;
78
68
/// Create the client again
79
- client = new BoxClient ( config , auth ) ;
80
- _logger . LogInformation ( "Refreshed Tokens" ) ;
69
+ client = new BoxClient ( _config , auth ) ;
70
+ if ( accessToken . Value != auth . AccessToken )
71
+ {
72
+ _logger . LogInformation ( $ "Access Token Changed to ({ auth . AccessToken . Substring ( 4 ) } ...)") ;
73
+ }
74
+ else
75
+ {
76
+ _logger . LogInformation ( $ "Access Token Unchanged ({ auth . AccessToken . Substring ( 4 ) } ...)") ;
77
+ }
78
+ if ( refreshToken . Value != auth . RefreshToken )
79
+ {
80
+ _logger . LogInformation ( $ "Refresh Token Changed to ({ auth . RefreshToken . Substring ( 4 ) } ...") ;
81
+ }
82
+ else
83
+ {
84
+ _logger . LogInformation ( $ "Refresh Token Unchanged ({ auth . RefreshToken . Substring ( 4 ) } ...") ;
85
+ }
86
+
81
87
accessToken . Value = auth . AccessToken ;
82
88
refreshToken . Value = auth . RefreshToken ;
83
89
await _context . SaveChangesAsync ( ) ;
90
+ _logger . LogInformation ( "RefreshAccessTokenAsync: Complete (database updated)" ) ;
84
91
}
85
92
}
86
93
catch ( Box . V2 . Exceptions . BoxSessionInvalidatedException e )
@@ -89,24 +96,111 @@ public async Task RefreshAccessTokenAsync()
89
96
await _slack . PostErrorAsync ( e , "Box Token Failure." ) ;
90
97
throw ;
91
98
}
99
+ _logger . LogInformation ( "RefreshAccessTokenAsync: returning" ) ;
100
+
101
+ }
102
+ public async Task < ( Dictionary , Dictionary ) > getOrCreateDatabaseEntries ( CTDbContext context )
103
+ {
104
+ // sanity check- expect 0 or 1 entries for key
105
+ if ( await context . Dictionaries . Where ( d=> d . Key == CommonUtils . BOX_ACCESS_TOKEN ) . CountAsync ( ) > 1
106
+ || await context . Dictionaries . Where ( d=> d . Key == CommonUtils . BOX_REFRESH_TOKEN ) . CountAsync ( ) > 1 )
107
+ { // should never happen
108
+ var badEntries = context . Dictionaries . Where ( d=> d . Key == CommonUtils . BOX_ACCESS_TOKEN || d . Key == CommonUtils . BOX_REFRESH_TOKEN ) ;
109
+ context . Dictionaries . RemoveRange ( badEntries ) ;
110
+ await context . SaveChangesAsync ( ) ;
111
+ }
112
+ var changed = false ;
113
+
114
+ var accessToken = await context . Dictionaries . Where ( d => d . Key == CommonUtils . BOX_ACCESS_TOKEN ) . FirstOrDefaultAsync ( ) ;
115
+ var refreshToken = await context . Dictionaries . Where ( d => d . Key == CommonUtils . BOX_REFRESH_TOKEN ) . FirstOrDefaultAsync ( ) ;
116
+
117
+ if ( accessToken == null )
118
+ {
119
+ accessToken = new Dictionary
120
+ {
121
+ Key = CommonUtils . BOX_ACCESS_TOKEN
122
+ } ;
123
+ context . Dictionaries . Add ( accessToken ) ;
124
+ changed = true ;
125
+ }
126
+ if ( refreshToken == null )
127
+ {
128
+ refreshToken = new Dictionary
129
+ {
130
+ Key = CommonUtils . BOX_REFRESH_TOKEN
131
+ } ;
132
+ context . Dictionaries . Add ( refreshToken ) ;
133
+ changed = true ;
134
+ }
135
+ if ( changed )
136
+ {
137
+ await context . SaveChangesAsync ( ) ;
138
+ }
139
+ return ( accessToken , refreshToken ) ;
92
140
}
141
+
93
142
/// <summary>
94
143
/// Creates a new box client, after first refreshing the access and refresh token.
95
144
/// </summary>
96
145
public async Task < BoxClient > GetBoxClientAsync ( )
97
146
{
98
147
// Todo RefreshAccessTokenAsync could return this information for us; and avoid another trip to the database
99
- await RefreshAccessTokenAsync ( ) ;
100
- BoxClient boxClient ;
101
- using ( var _context = CTDbContext . CreateDbContext ( ) )
148
+ int attempt = 1 ;
149
+ int maxAttempt = 10 ;
150
+ while ( attempt < maxAttempt )
102
151
{
103
- var accessToken = await _context . Dictionaries . Where ( d => d . Key == CommonUtils . BOX_ACCESS_TOKEN ) . FirstAsync ( ) ;
104
- var refreshToken = await _context . Dictionaries . Where ( d => d . Key == CommonUtils . BOX_REFRESH_TOKEN ) . FirstAsync ( ) ;
105
- var config = new BoxConfig ( Globals . appSettings . BOX_CLIENT_ID , Globals . appSettings . BOX_CLIENT_SECRET , new Uri ( "http://locahost" ) ) ;
106
- var auth = new OAuthSession ( accessToken . Value , refreshToken . Value , 3600 , "bearer" ) ;
107
- boxClient = new Box . V2 . BoxClient ( config , auth ) ;
152
+ using ( var _context = CTDbContext . CreateDbContext ( ) )
153
+ {
154
+ _logger . LogInformation ( $ "GetBoxClientAsync: Attempt { attempt } of { maxAttempt } to get valid client") ;
155
+ // var accessToken = await _context.Dictionaries.Where(d => d.Key == CommonUtils.BOX_ACCESS_TOKEN).FirstOrDefaultAsync();
156
+ //Dictionary accessToken,refreshToken;
157
+
158
+ var ( accessToken , refreshToken ) = await getOrCreateDatabaseEntries ( _context ) ;
159
+
160
+ if ( string . IsNullOrEmpty ( accessToken . Value ) )
161
+ {
162
+ _logger . LogInformation ( $ "GetBoxClientAsync: Attempting box client using access token ({ accessToken . Value . Substring ( 4 ) } ...") ;
163
+ try
164
+ {
165
+ var auth = new OAuthSession ( accessToken . Value , "" , 3600 , "bearer" ) ;
166
+
167
+ var client = new BoxClient ( _config , auth ) ;
168
+ _logger . LogInformation ( $ "GetBoxClientAsync: Attempt { attempt } returning client using existing access token") ;
169
+ return client ; // Normal return here
170
+ }
171
+ catch ( Exception e )
172
+ {
173
+ _logger . LogInformation ( e , "GetBoxClientAsync: Existing access token is invalid" ) ;
174
+ }
175
+ }
176
+ }
177
+ if ( refreshing > 0 )
178
+ {
179
+ var sleep = 5 + 5 * attempt ;
180
+ _logger . LogInformation ( $ "GetBoxClientAsync: refresh in progress - Sleeping { sleep } seconds - Give time for another thread to refresh the token before retrying") ;
181
+ await Task . Delay ( sleep * 1000 ) ;
182
+ }
183
+ else
184
+ {
185
+ Interlocked . Increment ( ref refreshing ) ; // threadsafe refreshing ++;
186
+ _logger . LogInformation ( $ "GetBoxClientAsync: Calling RefreshAccessTokenAsync") ;
187
+ await RefreshAccessTokenAsync ( ) ;
188
+ Interlocked . Decrement ( ref refreshing ) ;
189
+ }
108
190
}
109
- return boxClient ;
191
+ _logger . LogError ( "Failed to authenticate with Box" ) ;
192
+ throw new Exception ( "Failed to authenticate with Box" ) ;
193
+ // BoxClient boxClient;
194
+ // using (var _context = CTDbContext.CreateDbContext())
195
+ // {
196
+ // var accessToken = await _context.Dictionaries.Where(d => d.Key == CommonUtils.BOX_ACCESS_TOKEN).FirstAsync();
197
+ // var refreshToken = await _context.Dictionaries.Where(d => d.Key == CommonUtils.BOX_REFRESH_TOKEN).FirstAsync();
198
+ // var config = new BoxConfig(Globals.appSettings.BOX_CLIENT_ID, Globals.appSettings.BOX_CLIENT_SECRET, new Uri("http://locahost"));
199
+ // var auth = new OAuthSession(accessToken.Value, refreshToken.Value, 3600, "bearer");
200
+ // boxClient = new Box.V2.BoxClient(config, auth);
201
+ // }
202
+ // return boxClient;
110
203
}
111
204
}
112
205
}
206
+
0 commit comments