1
- using Box . V2 ;
1
+ using System ;
2
+ using System . Linq ;
3
+ using System . Threading . Tasks ;
4
+ using System . Threading ;
5
+
6
+ using Microsoft . EntityFrameworkCore ;
7
+ using Microsoft . Extensions . Logging ;
8
+
9
+ using Box . V2 ;
2
10
using Box . V2 . Auth ;
3
11
using Box . V2 . Config ;
12
+
4
13
using ClassTranscribeDatabase . Models ;
5
- using Microsoft . EntityFrameworkCore ;
6
- using Microsoft . Extensions . Logging ;
7
- using System ;
8
- using System . Linq ;
9
- using System . Threading . Tasks ;
14
+
10
15
11
16
namespace ClassTranscribeDatabase . Services
12
17
{
13
18
public class BoxAPI
14
19
{
15
20
private readonly SlackLogger _slack ;
16
21
private readonly ILogger _logger ;
22
+ private BoxClient ? _boxClient ;
23
+ private DateTimeOffset _lastRefreshed = DateTimeOffset . MinValue ;
24
+ private SemaphoreSlim _RefreshSemaphore = new SemaphoreSlim ( 1 , 1 ) ; // async-safe mutex to ensure only one thread is refreshing the token at a time
25
+
17
26
public BoxAPI ( ILogger < BoxAPI > logger , SlackLogger slack )
18
27
{
19
28
_logger = logger ;
@@ -30,6 +39,7 @@ public async Task CreateAccessTokenAsync(string authCode)
30
39
// This implementation is overly chatty with the database, but we rarely create access tokens so it is not a problem
31
40
using ( var _context = CTDbContext . CreateDbContext ( ) )
32
41
{
42
+
33
43
if ( ! await _context . Dictionaries . Where ( d => d . Key == CommonUtils . BOX_ACCESS_TOKEN ) . AnyAsync ( ) )
34
44
{
35
45
_context . Dictionaries . Add ( new Dictionary
@@ -47,13 +57,15 @@ public async Task CreateAccessTokenAsync(string authCode)
47
57
await _context . SaveChangesAsync ( ) ;
48
58
}
49
59
50
-
60
+
51
61
var accessToken = _context . Dictionaries . Where ( d => d . Key == CommonUtils . BOX_ACCESS_TOKEN ) . First ( ) ;
52
62
var refreshToken = _context . Dictionaries . Where ( d => d . Key == CommonUtils . BOX_REFRESH_TOKEN ) . First ( ) ;
53
63
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 ) ;
55
- var auth = await client . Auth . AuthenticateAsync ( authCode ) ;
56
- _logger . LogInformation ( "Created Box Tokens" ) ;
64
+ var tmpClient = new Box . V2 . BoxClient ( config ) ;
65
+ var auth = await tmpClient . Auth . AuthenticateAsync ( authCode ) ;
66
+
67
+ _logger . LogInformation ( $ "Created Box Tokens Access:({ auth . AccessToken . Substring ( 0 , 5 ) } ) Refresh({ auth . RefreshToken . Substring ( 0 , 5 ) } )") ;
68
+
57
69
accessToken . Value = auth . AccessToken ;
58
70
refreshToken . Value = auth . RefreshToken ;
59
71
await _context . SaveChangesAsync ( ) ;
@@ -62,31 +74,37 @@ public async Task CreateAccessTokenAsync(string authCode)
62
74
/// <summary>
63
75
/// Updates the accessToken and refreshToken. These keys must already exist in the Dictionary table.
64
76
/// </summary>
65
- public async Task RefreshAccessTokenAsync ( )
77
+ private async Task < BoxClient > RefreshAccessTokenAsync ( )
66
78
{
79
+ // Only one thread should call this at a time (see semaphore in GetBoxClientAsync)
67
80
try
68
81
{
82
+ _logger . LogInformation ( $ "RefreshAccessTokenAsync: Starting") ;
69
83
using ( var _context = CTDbContext . CreateDbContext ( ) )
70
84
{
71
85
var accessToken = await _context . Dictionaries . Where ( d => d . Key == CommonUtils . BOX_ACCESS_TOKEN ) . FirstAsync ( ) ;
72
86
var refreshToken = await _context . Dictionaries . Where ( d => d . Key == CommonUtils . BOX_REFRESH_TOKEN ) . FirstAsync ( ) ;
73
87
var config = new BoxConfig ( Globals . appSettings . BOX_CLIENT_ID , Globals . appSettings . BOX_CLIENT_SECRET , new Uri ( "http://locahost" ) ) ;
74
- var auth = new OAuthSession ( accessToken . Value , refreshToken . Value , 3600 , "bearer" ) ;
75
- var client = new BoxClient ( config , auth ) ;
76
- /// Try to refresh the access token
77
- auth = await client . Auth . RefreshAccessTokenAsync ( auth . AccessToken ) ;
88
+ var initialAuth = new OAuthSession ( accessToken . Value , refreshToken . Value , 3600 , "bearer" ) ;
89
+ var initialClient = new BoxClient ( config , initialAuth ) ;
90
+ /// Refresh the access token
91
+ var auth = await initialClient . Auth . RefreshAccessTokenAsync ( initialAuth . AccessToken ) ;
78
92
/// Create the client again
79
- client = new BoxClient ( config , auth ) ;
80
- _logger . LogInformation ( "Refreshed Tokens" ) ;
93
+ _logger . LogInformation ( $ "RefreshAccessTokenAsync: New Access Token ( { auth . AccessToken . Substring ( 0 , 5 ) } ), New Refresh Token ( { auth . RefreshToken . Substring ( 0 , 5 ) } )" ) ;
94
+
81
95
accessToken . Value = auth . AccessToken ;
82
96
refreshToken . Value = auth . RefreshToken ;
97
+ _lastRefreshed = DateTimeOffset . Now ;
83
98
await _context . SaveChangesAsync ( ) ;
99
+ _logger . LogInformation ( $ "RefreshAccessTokenAsync: Creating New Box Client") ;
100
+ var client = new BoxClient ( config , auth ) ;
101
+ return client ;
84
102
}
85
103
}
86
104
catch ( Box . V2 . Exceptions . BoxSessionInvalidatedException e )
87
105
{
88
- _logger . LogError ( e , "Box Token Failure." ) ;
89
- await _slack . PostErrorAsync ( e , "Box Token Failure." ) ;
106
+ _logger . LogError ( e , "RefreshAccessTokenAsync: Box Token Failure." ) ;
107
+ await _slack . PostErrorAsync ( e , "RefreshAccessTokenAsync: Box Token Failure." ) ;
90
108
throw ;
91
109
}
92
110
}
@@ -95,18 +113,32 @@ public async Task RefreshAccessTokenAsync()
95
113
/// </summary>
96
114
public async Task < BoxClient > GetBoxClientAsync ( )
97
115
{
98
- // 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 ( ) )
116
+ try
102
117
{
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 ) ;
118
+ await _RefreshSemaphore . WaitAsync ( ) ; // // critical section : implementation of an async-safe mutex
119
+ var MAX_AGE_MINUTES = 50 ;
120
+ var remain = DateTimeOffset . Now . Subtract ( _lastRefreshed ) . TotalMinutes ;
121
+ _logger . LogInformation ( $ "GetBoxClientAsync: { remain } minutes since last refresh. Max age { MAX_AGE_MINUTES } .") ;
122
+ if ( _boxClient != null && remain < MAX_AGE_MINUTES )
123
+ {
124
+ return _boxClient ;
125
+ }
126
+ _boxClient = await RefreshAccessTokenAsync ( ) ;
127
+ _logger . LogInformation ( $ "GetBoxClientAsync: _boxClient updated") ;
128
+ }
129
+ catch ( Exception e )
130
+ {
131
+ _logger . LogError ( e , "GetBoxClientAsync: Box Refresh Failure." ) ;
132
+ throw ;
108
133
}
109
- return boxClient ;
134
+ finally
135
+ {
136
+ _logger . LogInformation ( $ "GetBoxClientAsync: Releasing Semaphore and returning") ;
137
+ _RefreshSemaphore . Release ( 1 ) ;
138
+ }
139
+
140
+ return _boxClient ;
110
141
}
142
+
111
143
}
112
144
}
0 commit comments