4
4
5
5
namespace Aydsko . iRacingData ;
6
6
7
- public class OAuthPasswordLimitedAuthenticatingHttpClient ( HttpClient httpClient ,
8
- iRacingDataClientOptions options ,
9
- TimeProvider timeProvider )
10
- : IDisposable , IAuthenticatingHttpClient
7
+ public abstract class OAuthAuthenticatingHttpClientBase ( HttpClient httpClient ,
8
+ iRacingDataClientOptions options ,
9
+ TimeProvider timeProvider )
10
+ : IDisposable
11
11
{
12
+ protected HttpClient HttpClient { get ; } = httpClient ;
13
+ protected iRacingDataClientOptions Options { get ; } = options ;
14
+ protected TimeProvider TimeProvider { get ; } = timeProvider ;
15
+
12
16
private readonly SemaphoreSlim loginSemaphore = new ( 1 , 1 ) ;
13
- private OAuthTokenResponse ? tokenResponse ;
17
+
14
18
private DateTimeOffset ? accessTokenExpiryInstantUtc ;
15
- private DateTimeOffset ? refreshTokenExpiryInstantUtc ;
16
19
private bool disposedValue ;
20
+ private DateTimeOffset ? refreshTokenExpiryInstantUtc ;
21
+ private OAuthTokenResponse ? tokenResponse ;
22
+
23
+ public void ClearLoggedInState ( )
24
+ {
25
+ tokenResponse = null ;
26
+ }
27
+
28
+ // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
29
+ // ~OAuthAuthenticatingHttpClientBase()
30
+ // {
31
+ // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
32
+ // Dispose(disposing: false);
33
+ // }
34
+
35
+ public void Dispose ( )
36
+ {
37
+ // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
38
+ Dispose ( disposing : true ) ;
39
+ GC . SuppressFinalize ( this ) ;
40
+ }
41
+
42
+ public async Task < HttpResponseMessage > SendAsync ( HttpRequestMessage request ,
43
+ HttpCompletionOption completionOption = HttpCompletionOption . ResponseContentRead ,
44
+ CancellationToken cancellationToken = default )
45
+ {
46
+ return await HttpClient . SendAsync ( request , completionOption , cancellationToken )
47
+ . ConfigureAwait ( false ) ;
48
+ }
17
49
18
50
public async Task < HttpResponseMessage > SendAuthenticatedRequestAsync ( HttpRequestMessage request ,
19
51
HttpCompletionOption completionOption = HttpCompletionOption . ResponseContentRead ,
@@ -34,17 +66,20 @@ public async Task<HttpResponseMessage> SendAuthenticatedRequestAsync(HttpRequest
34
66
return await SendAsync ( request , completionOption , cancellationToken ) . ConfigureAwait ( false ) ;
35
67
}
36
68
37
- public async Task < HttpResponseMessage > SendAsync ( HttpRequestMessage request ,
38
- HttpCompletionOption completionOption = HttpCompletionOption . ResponseContentRead ,
39
- CancellationToken cancellationToken = default )
69
+ protected virtual void Dispose ( bool disposing )
40
70
{
41
- return await httpClient . SendAsync ( request , completionOption , cancellationToken )
42
- . ConfigureAwait ( false ) ;
43
- }
71
+ if ( ! disposedValue )
72
+ {
73
+ if ( disposing )
74
+ {
75
+ // TODO: dispose managed state (managed objects)
76
+ loginSemaphore . Dispose ( ) ;
77
+ }
44
78
45
- public void ClearLoggedInState ( )
46
- {
47
- tokenResponse = null ;
79
+ // TODO: free unmanaged resources (unmanaged objects) and override finalizer
80
+ // TODO: set large fields to null
81
+ disposedValue = true ;
82
+ }
48
83
}
49
84
50
85
private async Task < string > GetAccessTokenAsync ( CancellationToken cancellationToken = default )
@@ -70,7 +105,7 @@ await loginSemaphore.WaitAsync(cancellationToken)
70
105
_ = loginSemaphore . Release ( ) ;
71
106
}
72
107
}
73
- else if ( accessTokenExpiryInstantUtc <= timeProvider . GetUtcNow ( ) )
108
+ else if ( accessTokenExpiryInstantUtc <= TimeProvider . GetUtcNow ( ) )
74
109
{
75
110
await loginSemaphore . WaitAsync ( cancellationToken )
76
111
. ConfigureAwait ( false ) ;
@@ -80,12 +115,12 @@ await loginSemaphore.WaitAsync(cancellationToken)
80
115
#pragma warning disable IDE0074 // Use compound assignment
81
116
82
117
// If the refresh token doesn't exist or is expired it is no good so we'll need to request a whole new token.
83
- if ( ( refreshTokenExpiryInstantUtc ?? DateTimeOffset . MinValue ) <= timeProvider . GetUtcNow ( ) )
118
+ if ( ( refreshTokenExpiryInstantUtc ?? DateTimeOffset . MinValue ) <= TimeProvider . GetUtcNow ( ) )
84
119
{
85
120
( tokenResponse , accessTokenExpiryInstantUtc , refreshTokenExpiryInstantUtc ) = await RequestTokenAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
86
121
return tokenResponse . AccessToken ;
87
122
}
88
- else if ( accessTokenExpiryInstantUtc <= timeProvider . GetUtcNow ( ) )
123
+ else if ( accessTokenExpiryInstantUtc <= TimeProvider . GetUtcNow ( ) )
89
124
{
90
125
( tokenResponse , accessTokenExpiryInstantUtc , refreshTokenExpiryInstantUtc ) = await RefreshTokenAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
91
126
return tokenResponse . AccessToken ;
@@ -100,79 +135,7 @@ await loginSemaphore.WaitAsync(cancellationToken)
100
135
}
101
136
}
102
137
103
- return tokenResponse . AccessToken ;
104
- }
105
-
106
- private async Task < ( OAuthTokenResponse Token , DateTimeOffset ExpiresAt , DateTimeOffset ? RefreshTokenExpiresAt ) > RequestTokenAsync ( CancellationToken cancellationToken = default )
107
- {
108
- using var activity = AydskoDataClientDiagnostics . ActivitySource . StartActivity ( "Retrieve \" password_limited\" token" , System . Diagnostics . ActivityKind . Client ) ;
109
-
110
- try
111
- {
112
- if ( string . IsNullOrWhiteSpace ( options . Username )
113
- || string . IsNullOrWhiteSpace ( options . Password )
114
- || string . IsNullOrWhiteSpace ( options . ClientId )
115
- || string . IsNullOrWhiteSpace ( options . ClientSecret ) )
116
- {
117
- throw new InvalidOperationException ( "All of \" Username\" , \" Password\" , \" ClientId\" , and \" ClientSecret\" must be set in the options." ) ;
118
- }
119
-
120
- if ( string . IsNullOrWhiteSpace ( options . AuthServiceBaseUrl )
121
- || ! Uri . TryCreate ( options . AuthServiceBaseUrl , UriKind . Absolute , out var authServiceBaseUrl ) )
122
- {
123
- throw new InvalidOperationException ( "The \" AuthServiceBaseUrl\" must be a valid absolute URL in the iRacing Data Client options." ) ;
124
- }
125
-
126
- var encodedPassword = options . PasswordIsEncoded ? options . Password : ApiClient . EncodePassword ( options . Username ! , options . Password ! ) ;
127
- var encodedClientSecret = options . ClientSecretIsEncoded ? options . ClientSecret : ApiClient . EncodePassword ( options . ClientId ! , options . ClientSecret ! ) ;
128
-
129
- using var newTokenRequest = new HttpRequestMessage ( HttpMethod . Post ,
130
- new Uri ( authServiceBaseUrl , "/oauth2/token" ) )
131
- {
132
- Content = new FormUrlEncodedContent (
133
- [
134
- new ( "grant_type" , "password_limited" ) ,
135
- new ( "client_id" , options . ClientId ) ,
136
- new ( "client_secret" , encodedClientSecret ) ,
137
- new ( "username" , options . Username ) ,
138
- new ( "password" , encodedPassword ) ,
139
- new ( "scope" , "iracing.auth iracing.profile" ) ,
140
- ] ) ,
141
- } ;
142
-
143
- var newTokenResponse = await httpClient . SendAsync ( newTokenRequest , HttpCompletionOption . ResponseHeadersRead , cancellationToken )
144
- . ConfigureAwait ( false ) ;
145
- var utcNow = timeProvider . GetUtcNow ( ) ;
146
-
147
- if ( ! newTokenResponse . IsSuccessStatusCode )
148
- {
149
- #if NET6_0_OR_GREATER
150
- var errorContent = await newTokenResponse . Content . ReadAsStringAsync ( cancellationToken )
151
- . ConfigureAwait ( false ) ;
152
- #else
153
- var errorContent = await newTokenResponse . Content . ReadAsStringAsync ( )
154
- . ConfigureAwait ( false ) ;
155
- #endif
156
- throw new iRacingLoginFailedException ( $ "Attempt to retrieve \" password_limited\" OAuth token failed with status code { newTokenResponse . StatusCode } and content: { errorContent } ") ;
157
- }
158
-
159
- var token = await newTokenResponse . Content . ReadFromJsonAsync < OAuthTokenResponse > ( cancellationToken )
160
- . ConfigureAwait ( false )
161
- ?? throw new iRacingLoginFailedException ( "Failed to deserialize OAuth token response from iRacing API." ) ;
162
-
163
- var expiresAt = utcNow . AddSeconds ( token . ExpiresInSeconds ) ;
164
- var refreshExpiresAt = token . RefreshTokenExpiresInSeconds != null ? utcNow . AddSeconds ( token . RefreshTokenExpiresInSeconds . Value ) : ( DateTimeOffset ? ) null ;
165
-
166
- activity ? . SetStatus ( System . Diagnostics . ActivityStatusCode . Ok , "Token retrieved successfully" ) ;
167
-
168
- return ( token , expiresAt , refreshExpiresAt ) ;
169
- }
170
- catch ( Exception ex )
171
- {
172
- activity ? . AddException ( ex ) ;
173
- activity ? . SetStatus ( System . Diagnostics . ActivityStatusCode . Error , "Exception thrown retrieving token" ) ;
174
- throw ;
175
- }
138
+ return tokenResponse . AccessToken ;
176
139
}
177
140
178
141
private async Task < ( OAuthTokenResponse Token , DateTimeOffset ExpiresAt , DateTimeOffset ? RefreshTokenExpiresAt ) > RefreshTokenAsync ( CancellationToken cancellationToken = default )
@@ -181,14 +144,14 @@ await loginSemaphore.WaitAsync(cancellationToken)
181
144
182
145
try
183
146
{
184
- if ( string . IsNullOrWhiteSpace ( options . ClientId )
185
- || string . IsNullOrWhiteSpace ( options . ClientSecret ) )
147
+ if ( string . IsNullOrWhiteSpace ( Options . ClientId )
148
+ || string . IsNullOrWhiteSpace ( Options . ClientSecret ) )
186
149
{
187
150
throw new InvalidOperationException ( "All of \" ClientId\" and \" ClientSecret\" must be set in the options." ) ;
188
151
}
189
152
190
- if ( string . IsNullOrWhiteSpace ( options . AuthServiceBaseUrl )
191
- || ! Uri . TryCreate ( options . AuthServiceBaseUrl , UriKind . Absolute , out var authServiceBaseUrl ) )
153
+ if ( string . IsNullOrWhiteSpace ( Options . AuthServiceBaseUrl )
154
+ || ! Uri . TryCreate ( Options . AuthServiceBaseUrl , UriKind . Absolute , out var authServiceBaseUrl ) )
192
155
{
193
156
throw new InvalidOperationException ( "The \" AuthServiceBaseUrl\" must be a valid absolute URL in the iRacing Data Client options." ) ;
194
157
}
@@ -199,23 +162,23 @@ await loginSemaphore.WaitAsync(cancellationToken)
199
162
throw new InvalidOperationException ( "Previous response must have contained a refresh token value." ) ;
200
163
}
201
164
202
- var encodedClientSecret = options . ClientSecretIsEncoded ? options . ClientSecret : ApiClient . EncodePassword ( options . ClientId ! , options . ClientSecret ! ) ;
165
+ var encodedClientSecret = Options . ClientSecretIsEncoded ? Options . ClientSecret : ApiClient . EncodePassword ( Options . ClientId ! , Options . ClientSecret ! ) ;
203
166
204
167
using var newTokenRequest = new HttpRequestMessage ( HttpMethod . Post ,
205
168
new Uri ( authServiceBaseUrl , "/oauth2/token" ) )
206
169
{
207
170
Content = new FormUrlEncodedContent (
208
171
[
209
172
new ( "grant_type" , "refresh_token" ) ,
210
- new ( "client_id" , options . ClientId ) ,
173
+ new ( "client_id" , Options . ClientId ) ,
211
174
new ( "client_secret" , encodedClientSecret ) ,
212
175
new ( "refresh_token" , refreshToken ) ,
213
176
] ) ,
214
177
} ;
215
178
216
- var newTokenResponse = await httpClient . SendAsync ( newTokenRequest , HttpCompletionOption . ResponseHeadersRead , cancellationToken )
179
+ var newTokenResponse = await HttpClient . SendAsync ( newTokenRequest , HttpCompletionOption . ResponseHeadersRead , cancellationToken )
217
180
. ConfigureAwait ( false ) ;
218
- var utcNow = timeProvider . GetUtcNow ( ) ;
181
+ var utcNow = TimeProvider . GetUtcNow ( ) ;
219
182
220
183
if ( ! newTokenResponse . IsSuccessStatusCode )
221
184
{
@@ -248,33 +211,5 @@ await loginSemaphore.WaitAsync(cancellationToken)
248
211
}
249
212
}
250
213
251
- protected virtual void Dispose ( bool disposing )
252
- {
253
- if ( ! disposedValue )
254
- {
255
- if ( disposing )
256
- {
257
- // TODO: dispose managed state (managed objects)
258
- loginSemaphore . Dispose ( ) ;
259
- }
260
-
261
- // TODO: free unmanaged resources (unmanaged objects) and override finalizer
262
- // TODO: set large fields to null
263
- disposedValue = true ;
264
- }
265
- }
266
-
267
- // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
268
- // ~OAuthPasswordLimitedAuthenticatingHttpClient()
269
- // {
270
- // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
271
- // Dispose(disposing: false);
272
- // }
273
-
274
- public void Dispose ( )
275
- {
276
- // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
277
- Dispose ( disposing : true ) ;
278
- GC . SuppressFinalize ( this ) ;
279
- }
214
+ protected abstract Task < ( OAuthTokenResponse Token , DateTimeOffset ExpiresAt , DateTimeOffset ? RefreshTokenExpiresAt ) > RequestTokenAsync ( CancellationToken cancellationToken = default ) ;
280
215
}
0 commit comments