C# Client Library
A C# Client Library for the AnalyzeRe REST API
Loading...
Searching...
No Matches
API.Authentication.cs
Go to the documentation of this file.
1using System;
2using System.Collections.Specialized;
3using System.Diagnostics.CodeAnalysis;
4using System.Linq;
5using System.Net;
6using System.Security.Authentication;
7using System.Threading;
8
11using RestSharp;
12
13namespace AnalyzeRe
14{
18 public static partial class API
19 {
20 private const string AUTH_HEADER = "Authorization";
21
29 public delegate void AuthenticationRequestedHandler(
30 ref IAccessToken ToAuthenticate,
31 string RequestedAuthentication);
32
43 [SuppressMessage("Microsoft.Design", "CA1009:DeclareEventHandlersCorrectly")]
45
48 public static IAccessToken AuthenticationToken { get; set; }
49
53 public static string GetCurrentAuthorizationString()
54 {
55 return AuthenticationToken == null ? null :
56 AuthenticationToken.AuthorizationMethod + " " + AuthenticationToken.access_token;
57 }
58
82 public static bool TryHandleAuthenticationErrors(IRestResponse response,
83 int sequentialAttempts = 0)
84 {
85 // There is nothing authentication-related about an incomplete response,
86 // so if the response is not completed, we're done here.
87 if (response.ResponseStatus != ResponseStatus.Completed)
88 {
89 // Special case. Due to an only recently patched Microsoft bug (KB3109853)
90 // Rare SSL/TLS failures may occur that aren't actually indicative of a problem.
91 // Retry unless we've had MaxRequestRetries failures in a row.
92 if (response.ErrorException is WebException &&
93 response.ErrorException.Message.Contains("Could not create SSL/TLS secure channel"))
94 {
95 // If this keeps happening, it's a real error, so throw.
96 // Otherwise, it may just be a sporadic error, so retry.
97 if (sequentialAttempts < MaxRequestRetries) return true;
98 throw new AuthenticationException(response.ErrorMessage, response.ErrorException);
99 }
100 return false;
101 }
102 // Otherwise, this request completed its round trip to the server.
103 // Now determine whether this request's response was generally successful
104 bool success = (int)response.StatusCode >= 200 && (int)response.StatusCode < 300;
105
106 // There's nothing more to do if the request was successful and the
107 // AuthenticationToken is either not set or already marked as valid.
108 if (success && (AuthenticationToken == null ||
110 return false;
111
112 // Otherwise, we want to capture the authentication status for future reference.
113 // Get the currently configured global Authentication token
114 string defaultAuthToken = GetCurrentAuthorizationString();
115 // Get the authentication token supplied in this response's original request.
116 string requestAuthenticationToken = response.Request.Parameters.Where(
117 p => p.Type == ParameterType.HttpHeader &&
118 p.Name.Equals(AUTH_HEADER, StringComparison.OrdinalIgnoreCase))
119 .Select(p => p.Value.ToString()).FirstOrDefault();
120
121 // Test to see whether the previous request used the global default authentication
122 // token (as opposed to having supplied an overriding authentication header)
123 bool usedDefaultAuth = requestAuthenticationToken == defaultAuthToken;
124 // If they did use the global AuthenticationToken setting, set its status
125 if (usedDefaultAuth && AuthenticationToken != null)
126 {
127 // Only mark is as valid if the request response was a success
128 if (success)
129 AuthenticationToken.status = AccessTokenStatus.Valid;
130 // Only mark it as unauthorized if that was the status code
131 else if (response.StatusCode == HttpStatusCode.Unauthorized)
132 AuthenticationToken.status = AccessTokenStatus.Unauthorized;
133 // Otherwise, leave the AuthenticationToken status unchanged
134 }
135
136 // Now, unless the response code was specifically an authorization error, we're done.
137 if (response.StatusCode != HttpStatusCode.Unauthorized)
138 return false;
139 // Otherwise, the request failed authentication. We try to handle this.
140
141 // Check if we have exceeded our maximum authentication attempts for a single request.
142 if (sequentialAttempts > MaxRequestRetries)
143 {
144 throw new AuthenticationException("Request failed after " +
145 MaxRequestRetries + " failed authentication attempts.");
146 }
147
148 // If the user supplied their own Authorization header (rather than letting the client
149 // library automatically fill in the API.AuthenticationToken), we cannot raise the
150 // event that would prompt for new default authentication credentials, because it
151 // doesn't impact requests with manually-supplied Authorization headers.
152
153 // Invoke AuthenticationRequested if the user did not override the global default
154 // authentication parameters for this request.
155 if (usedDefaultAuth)
156 {
157 // Get the authentication header returned by the server
158 Parameter authenticateHeader =
159 response.Headers.FirstOrDefault(h => h.Name == "WWW-Authenticate");
160 string strAuthenticateHeader = authenticateHeader?.Value?.ToString();
161
162 // Invoke a request to authenticate and set the global authentication token
163 InvokeAuthenticationRequested(requestAuthenticationToken, strAuthenticateHeader);
164 // Return true to indicate that the authentication event was handled
165 // and the original request can be retried with the new authentication information.
166 return true;
167 }
168
169 // Otherwise, the user manually supplied an Authentication header, in which case they
170 // are not using the 'AuthenticationRequested' event, so we must return an error.
171 string exceptionMessage = "The current request included an authentication token (" +
172 requestAuthenticationToken + ") which was rejected by the server";
173 // If there was an existing global default authentication token that the user
174 // overrode for this request, and authentication failed, give a special error.
175 if (defaultAuthToken != null)
176 {
177 exceptionMessage += ", and which differs from the currently configured default " +
178 "authentication token (" + defaultAuthToken + "). Either correct the \"" +
179 AUTH_HEADER + "\" header parameter supplied to this request, or exclude it " +
180 "entirely and the default authentication token will be used.";
181 }
182 // Otherwise, throw an AuthenticationException with a slightly different message.
183 else
184 {
185 exceptionMessage += ". Either correct the \"" + AUTH_HEADER + "\" header parameter " +
186 "supplied to this request, or exclude it entirely and use the " +
187 "'API.AuthenticationRequested' event or 'API.AuthenticationToken' " +
188 "to automatically handle authentication for Analyze Re API requests.";
189 }
190 throw new AuthenticationException(exceptionMessage);
191 }
192
193 // Prevent more than one thread prompting the client for authentication at a time.
194 private static readonly object InvokeAuthenticationRequestedLock = new object();
195
205 private static void InvokeAuthenticationRequested(
206 string authenticationTokenUsed,
207 string RequestedAuthentication)
208 {
209 if (RequestedAuthentication == null)
210 {
211 throw new AuthenticationException("Authentication requested, but no " +
212 "authentication type was specified. This is a server error.");
213 }
214
215 bool lockTaken = false;
216 try
217 {
218 // If more than one thread is waiting on authentication, the other threads will
219 // time out after 30 seconds (in case the first thread gets hung).
220 Monitor.TryEnter(InvokeAuthenticationRequestedLock, 30000, ref lockTaken);
221 if (AuthenticationToken != null &&
222 authenticationTokenUsed != GetCurrentAuthorizationString())
223 {
224 // Global configured authentication credentials have already been updated since
225 // the request that fired this event. Do not prompt for credentials again.
226 return;
227 }
228 if (!lockTaken)
229 {
230 throw new AuthenticationException(
231 "Timed out while waiting for another thread to complete authentication.");
232 }
233
234 if (AuthenticationRequested == null)
235 {
236 if (AuthenticationToken != null)
237 {
238 // This indicates that the user is setting the AuthenticationToken directly
239 // so they are setting it incorrectly.
240 throw new AuthenticationException("The server requires authentication, " +
241 "and the current authentication token (" +
242 GetCurrentAuthorizationString() + ") was rejected. " +
243 "Please verify the credentials supplied.");
244 }
245 // Otherwise, they have not supplied any form of authentication and should be
246 // informed of the various ways they can do so.
247 throw new AuthenticationException("The server requires authorization, " +
248 "but no authentication settings have been configured. Please either " +
249 "handle the API.AuthenticationRequested event appropriately, " +
250 "or set the global default API.AuthenticationToken directly.");
251 }
252
253 // Prepare a new authentication token to pass by reference to the event handler
254 IAccessToken newAuthenticationToken;
255 // Create the appropriate authentication token type.
256 if (RequestedAuthentication.Trim().StartsWith("Basic",
257 StringComparison.InvariantCultureIgnoreCase))
258 {
259 newAuthenticationToken = new BasicAuthenticationToken();
260 }
261 else if (RequestedAuthentication.Trim().StartsWith("OAuth",
262 StringComparison.InvariantCultureIgnoreCase))
263 {
264 newAuthenticationToken = new OauthAccessToken().Post();
265 }
266 else
267 {
268 throw new AuthenticationException(
269 "Unrecognized authentication type requested: " + RequestedAuthentication);
270 }
271
272 try
273 {
274 AuthenticationRequested(ref newAuthenticationToken, RequestedAuthentication);
275 }
276 catch (Exception ex)
277 {
278 throw new AuthenticationException("Error while invoking the " +
279 "AuthenticationRequested event: " + ex.Message, ex);
280 }
281 // AuthenticationToken was not configured correctly by the handler of the event
282 if (newAuthenticationToken?.access_token == null)
283 {
284 throw new AuthenticationException("The server requires authentication.");
285 }
286 // Otherwise, overwrite the current global authentication token with the new one.
287 AuthenticationToken = newAuthenticationToken;
288 CacheAuthentication();
289 }
290 finally
291 {
292 if (lockTaken)
293 Monitor.Exit(InvokeAuthenticationRequestedLock);
294 }
295 }
296
297 // Use application settings to cache authenticated tokens.
298 // Does not store passwords, just URLs and server access tokens.
299 private static void CacheAuthentication()
300 {
301 try
302 {
303 if (AuthenticationToken == null)
304 {
305 return;
306 }
307 TrySaveLibrarySettings(() =>
308 {
310 Settings.Default.CachedCredentials = new StringCollection();
311 // Check for prior credentials to be overwritten
312 string prior = Settings.Default.CachedCredentials.Cast<string>()
313 .FirstOrDefault(s => s.StartsWith(ServerURL));
314 if (prior != null) Settings.Default.CachedCredentials.Remove(prior);
315
316 Settings.Default.CachedCredentials.Add(String.Join("|", new[]
317 {
318 ServerURL,
319 AuthenticationToken is BasicAuthenticationToken ? "Basic" : "OAuth",
320 AuthenticationToken.access_token
321 }));
322 });
323 }
324 catch (Exception e)
325 {
326 throw new Exception("Error storing authentication credentials: " + e.Message, e);
327 }
328 }
329
330 // Check if the current server has cached credentials, and attempt to restore them.
331 private static void RestoreCachedAuthentication()
332 {
333 // Don't override active credentials if they've already been specified.
334 if (AuthenticationToken != null) return;
335 StringCollection cachedCredentials =
336 TryGetLibrarySettingObj(() => Settings.Default.CachedCredentials);
337 // Nothing we can do if the cache is empty
338 if (cachedCredentials == null) return;
339 // Search the cached credentials for this server URL.
340 string found = cachedCredentials.Cast<string>()
341 .FirstOrDefault(s => s.StartsWith(ServerURL + "|"));
342 if (found == null) return;
343 string[] credentials = found.Split('|');
344 // Create an instance of the correct AccessToken type
345 AuthenticationToken = credentials[1] == "Basic"
347 : new OauthAccessToken();
348 AuthenticationToken.access_token = credentials[2];
349 AuthenticationToken.status = AccessTokenStatus.Unknown;
350 // Verify these credentials are still valid
351 // First, ensure that we can connect to the server.
353 {
354 return;
355 }
356 // Check if these credentials result in successful authentication
357 AuthenticationStatus authenticationStatus = GetAuthenticationStatus();
358 if (authenticationStatus != AuthenticationStatus.Authenticated)
359 {
360 AuthenticationToken = null;
361 TrySaveLibrarySettings(() =>
362 {
363 // These credentials are invalid or not necessary, remove them from our cache.
364 Settings.Default.CachedCredentials.Remove(found);
365 });
366 }
367 }
368
372 public static void TryPromptForAuthentication()
373 {
374 // Hack: Make a small request that will require authentication.
375 // If not already authenticated, this should trigger the API request's
376 // `TryHandleAuthenticationErrors` to see the `Unauthorized` status and kick off
377 // the AuthenticationRequested event, which the client should be handling.
378 ExecuteRestRequest("", Method.HEAD);
379 }
380
388 public static AuthenticationStatus GetAuthenticationStatus(bool force_request = false)
389 {
390 if (!force_request && AuthenticationToken != null &&
392 return AuthenticationStatus.Authenticated;
393 // Make a small request that will require authentication, but return
394 // the IRestResponse directly rather than letting the API handle the
395 // 'Unauthorized' error, so we can test whether authentication is required or not.
396 IRestResponse response;
397 try
398 {
399 response = ExecuteRestRequest("", Method.HEAD, returnRawResponse: true);
400 }
401 catch (Exception)
402 {
403 return AuthenticationStatus.Error;
404 }
405
406 if (response.ResponseStatus != ResponseStatus.Completed)
407 return AuthenticationStatus.Error;
408
409 if (response.StatusCode == HttpStatusCode.Unauthorized)
410 {
411 if (AuthenticationToken == null)
412 return AuthenticationStatus.AuthenticationRequired;
413 AuthenticationToken.status = AccessTokenStatus.Unauthorized;
414 return AuthenticationStatus.InvalidCredentials;
415 }
416 // Any 200 code indicates a success
417 if (((int)response.StatusCode) / 100 == 2)
418 {
419 if (AuthenticationToken == null)
420 return AuthenticationStatus.NoAuthenticationRequired;
421 AuthenticationToken.status = AccessTokenStatus.Valid;
422 return AuthenticationStatus.Authenticated;
423 }
424
425 // The request completed, but returned an error other than Unauthorized.
426 return AuthenticationStatus.Unknown;
427 }
428
438 public static void ClearCachedAuthenticationCredentials(string server = null)
439 {
440 TrySaveLibrarySettings(() =>
441 {
442 // Nothing to do if the cache is empty
444 return;
445 if (server == null)
447 else
448 {
449 // Search the cached credentials for this server URL.
450 string found = Settings.Default.CachedCredentials.Cast<string>()
451 .FirstOrDefault(s => s.StartsWith(server + "|"));
452 if (found == null) return;
453 Settings.Default.CachedCredentials.Remove(found);
454 }
455 });
456 }
457 }
458}
API methods / requests made available to the user.
delegate void AuthenticationRequestedHandler(ref IAccessToken ToAuthenticate, string RequestedAuthentication)
A handler that can respond to a AuthenticationRequestedHandler event.
static AuthenticationRequestedHandler AuthenticationRequested
An event that fires whenever authentication is requested by the server. This event can be used to pro...
static IAccessToken AuthenticationToken
The AccessToken storing authentication information for requests made to the server.
static volatile int MaxRequestRetries
When a temporary communication failure occurs (such as a socket error or an authentication failure fo...
static bool IsAReServerActive(string checkServerURL, int? timeout=null)
Determines if the provided URL is a valid running Analyze Re server.
static void TryPromptForAuthentication()
Invoke the AuthenticationRequested event, which requests authentication credentials for the server....
static bool TryHandleAuthenticationErrors(IRestResponse response, int sequentialAttempts=0)
Checks an IRestResponse object for authentication errors, then invokes configured authentication dele...
static AuthenticationStatus GetAuthenticationStatus(bool force_request=false)
Determines whether we are currently authenticated against the server with the global default Authenti...
static string GetCurrentAuthorizationString()
Return the Authorization header string formed by the current AuthenticationToken.
static string ServerURL
The default server URL to be used for all API requests. On change, voids the current cached authentic...
static void ClearCachedAuthenticationCredentials(string server=null)
The Analyze Re API .NET Client Library caches authentication tokens that have been validated via the ...
static IRestResponse ExecuteRestRequest(string resource, Method method, IEnumerable< Parameter > requestParameters=null, int? timeout=null, bool returnRawResponse=false, Action< Stream > responseStreamReader=null)
Perform a REST request on the server.
An AccessToken storing basic authentication information for requests made to the server.
An AccessToken storing OAuth authentication information for requests made to the server.
This part of the Settings class is not generated from the Settings.settings designer,...
Definition Settings.cs:11
static Settings Default
The default settings used by the static Analyze Re API client library.
global::System.Collections.Specialized.StringCollection CachedCredentials
A collection of cached authentication tokens which can be used to restore a previous connection betwe...
An AccessToken storing authentication information for requests made to the server.
string access_token
The AccessToken identifier.
AccessTokenStatus status
The AccessToken's current AccessTokenStatus (unauthorized / valid).
AccessTokenStatus
An AccessToken's status (unauthorized / valid).
AuthenticationStatus
The status of authentication against the current server.