C# Client Library
A C# Client Library for the AnalyzeRe REST API
API.Authentication.cs
Go to the documentation of this file.
1 using System;
2 using System.Collections.Specialized;
3 using System.Diagnostics.CodeAnalysis;
4 using System.Linq;
5 using System.Net;
6 using System.Security.Authentication;
7 using System.Threading;
8 
10 using AnalyzeRe.Properties;
11 using RestSharp;
12 
13 namespace 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")]
44  public static event AuthenticationRequestedHandler AuthenticationRequested;
45 
48  public static IAccessToken AuthenticationToken { get; set; }
49 
53  public static string GetCurrentAuthorizationString()
54  {
55  return AuthenticationToken == null ? null :
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)
130  // Only mark it as unauthorized if that was the status code
131  else if (response.StatusCode == HttpStatusCode.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  {
309  if (Settings.Default.CachedCredentials == null)
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];
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;
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;
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
443  if (Settings.Default.CachedCredentials == null)
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 }
static void TryPromptForAuthentication()
Invoke the AuthenticationRequested event, which requests authentication credentials for the server...
global::System.Collections.Specialized.StringCollection CachedCredentials
A collection of cached authentication tokens which can be used to restore a previous connection betwe...
API methods / requests made available to the user.
static AuthenticationStatus GetAuthenticationStatus(bool force_request=false)
Determines whether we are currently authenticated against the server with the global default Authenti...
string access_token
The AccessToken identifier.
Definition: IAccessToken.cs:16
static string GetCurrentAuthorizationString()
Return the Authorization header string formed by the current AuthenticationToken.
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...
AccessTokenStatus
An AccessToken&#39;s status (unauthorized / valid).
This part of the Settings class is not generated from the Settings.settings designer, and only used to set the SettingsProvider for the class.
Definition: Settings.cs:10
static bool TryHandleAuthenticationErrors(IRestResponse response, int sequentialAttempts=0)
Checks an IRestResponse object for authentication errors, then invokes configured authentication dele...
AuthenticationStatus
The status of authentication against the current server.
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 authentication information for requests made to the server.
Definition: IAccessToken.cs:7
static void ClearCachedAuthenticationCredentials(string server=null)
The Analyze Re API .NET Client Library caches authentication tokens that have been validated via the ...
static string ServerURL
The default server URL to be used for all API requests. On change, voids the current cached authentic...
static volatile int MaxRequestRetries
When a temporary communication failure occurs (such as a socket error or an authentication failure fo...
AccessTokenStatus status
The AccessToken&#39;s current AccessTokenStatus (unauthorized / valid).
Definition: IAccessToken.cs:20
An AccessToken storing basic authentication information for requests made to the server.
static bool IsAReServerActive(string checkServerURL, int?timeout=null)
Determines if the provided URL is a valid running Analyze Re server.
static IAccessToken AuthenticationToken
The AccessToken storing authentication information for requests made to the server.
An AccessToken storing OAuth authentication information for requests made to the server.
string AuthorizationMethod
The Authorization Method included in the Authorization header.
Definition: IAccessToken.cs:12
static Settings Default
The default settings used by the static Analyze Re API client library.