44
55namespace Aydsko . iRacingData ;
66
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
1111{
12+ protected HttpClient HttpClient { get ; } = httpClient ;
13+ protected iRacingDataClientOptions Options { get ; } = options ;
14+ protected TimeProvider TimeProvider { get ; } = timeProvider ;
15+
1216 private readonly SemaphoreSlim loginSemaphore = new ( 1 , 1 ) ;
13- private OAuthTokenResponse ? tokenResponse ;
17+
1418 private DateTimeOffset ? accessTokenExpiryInstantUtc ;
15- private DateTimeOffset ? refreshTokenExpiryInstantUtc ;
1619 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+ }
1749
1850 public async Task < HttpResponseMessage > SendAuthenticatedRequestAsync ( HttpRequestMessage request ,
1951 HttpCompletionOption completionOption = HttpCompletionOption . ResponseContentRead ,
@@ -34,17 +66,20 @@ public async Task<HttpResponseMessage> SendAuthenticatedRequestAsync(HttpRequest
3466 return await SendAsync ( request , completionOption , cancellationToken ) . ConfigureAwait ( false ) ;
3567 }
3668
37- public async Task < HttpResponseMessage > SendAsync ( HttpRequestMessage request ,
38- HttpCompletionOption completionOption = HttpCompletionOption . ResponseContentRead ,
39- CancellationToken cancellationToken = default )
69+ protected virtual void Dispose ( bool disposing )
4070 {
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+ }
4478
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+ }
4883 }
4984
5085 private async Task < string > GetAccessTokenAsync ( CancellationToken cancellationToken = default )
@@ -70,7 +105,7 @@ await loginSemaphore.WaitAsync(cancellationToken)
70105 _ = loginSemaphore . Release ( ) ;
71106 }
72107 }
73- else if ( accessTokenExpiryInstantUtc <= timeProvider . GetUtcNow ( ) )
108+ else if ( accessTokenExpiryInstantUtc <= TimeProvider . GetUtcNow ( ) )
74109 {
75110 await loginSemaphore . WaitAsync ( cancellationToken )
76111 . ConfigureAwait ( false ) ;
@@ -80,12 +115,12 @@ await loginSemaphore.WaitAsync(cancellationToken)
80115#pragma warning disable IDE0074 // Use compound assignment
81116
82117 // 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 ( ) )
84119 {
85120 ( tokenResponse , accessTokenExpiryInstantUtc , refreshTokenExpiryInstantUtc ) = await RequestTokenAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
86121 return tokenResponse . AccessToken ;
87122 }
88- else if ( accessTokenExpiryInstantUtc <= timeProvider . GetUtcNow ( ) )
123+ else if ( accessTokenExpiryInstantUtc <= TimeProvider . GetUtcNow ( ) )
89124 {
90125 ( tokenResponse , accessTokenExpiryInstantUtc , refreshTokenExpiryInstantUtc ) = await RefreshTokenAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
91126 return tokenResponse . AccessToken ;
@@ -100,79 +135,7 @@ await loginSemaphore.WaitAsync(cancellationToken)
100135 }
101136 }
102137
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 ;
176139 }
177140
178141 private async Task < ( OAuthTokenResponse Token , DateTimeOffset ExpiresAt , DateTimeOffset ? RefreshTokenExpiresAt ) > RefreshTokenAsync ( CancellationToken cancellationToken = default )
@@ -181,14 +144,14 @@ await loginSemaphore.WaitAsync(cancellationToken)
181144
182145 try
183146 {
184- if ( string . IsNullOrWhiteSpace ( options . ClientId )
185- || string . IsNullOrWhiteSpace ( options . ClientSecret ) )
147+ if ( string . IsNullOrWhiteSpace ( Options . ClientId )
148+ || string . IsNullOrWhiteSpace ( Options . ClientSecret ) )
186149 {
187150 throw new InvalidOperationException ( "All of \" ClientId\" and \" ClientSecret\" must be set in the options." ) ;
188151 }
189152
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 ) )
192155 {
193156 throw new InvalidOperationException ( "The \" AuthServiceBaseUrl\" must be a valid absolute URL in the iRacing Data Client options." ) ;
194157 }
@@ -199,23 +162,23 @@ await loginSemaphore.WaitAsync(cancellationToken)
199162 throw new InvalidOperationException ( "Previous response must have contained a refresh token value." ) ;
200163 }
201164
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 ! ) ;
203166
204167 using var newTokenRequest = new HttpRequestMessage ( HttpMethod . Post ,
205168 new Uri ( authServiceBaseUrl , "/oauth2/token" ) )
206169 {
207170 Content = new FormUrlEncodedContent (
208171 [
209172 new ( "grant_type" , "refresh_token" ) ,
210- new ( "client_id" , options . ClientId ) ,
173+ new ( "client_id" , Options . ClientId ) ,
211174 new ( "client_secret" , encodedClientSecret ) ,
212175 new ( "refresh_token" , refreshToken ) ,
213176 ] ) ,
214177 } ;
215178
216- var newTokenResponse = await httpClient . SendAsync ( newTokenRequest , HttpCompletionOption . ResponseHeadersRead , cancellationToken )
179+ var newTokenResponse = await HttpClient . SendAsync ( newTokenRequest , HttpCompletionOption . ResponseHeadersRead , cancellationToken )
217180 . ConfigureAwait ( false ) ;
218- var utcNow = timeProvider . GetUtcNow ( ) ;
181+ var utcNow = TimeProvider . GetUtcNow ( ) ;
219182
220183 if ( ! newTokenResponse . IsSuccessStatusCode )
221184 {
@@ -248,33 +211,5 @@ await loginSemaphore.WaitAsync(cancellationToken)
248211 }
249212 }
250213
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 ) ;
280215}
0 commit comments