SEBWIN-572: Improved stability of SEB Server connection by automatically updating OAuth2 token if it expires.
This commit is contained in:
parent
3277892ff2
commit
2875eb4c94
2 changed files with 126 additions and 57 deletions
|
@ -29,6 +29,25 @@ namespace SafeExamBrowser.Server
|
|||
this.logger = logger;
|
||||
}
|
||||
|
||||
internal bool IsTokenExpired(HttpContent content)
|
||||
{
|
||||
var isExpired = false;
|
||||
|
||||
try
|
||||
{
|
||||
var json = JsonConvert.DeserializeObject(Extract(content)) as JObject;
|
||||
var error = json["error"].Value<string>();
|
||||
|
||||
isExpired = error?.Equals("invalid_token", StringComparison.OrdinalIgnoreCase) == true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Error("Failed to parse token expiration content!", e);
|
||||
}
|
||||
|
||||
return isExpired;
|
||||
}
|
||||
|
||||
internal bool TryParseApi(HttpContent content, out ApiVersion1 api)
|
||||
{
|
||||
var success = false;
|
||||
|
@ -88,7 +107,7 @@ namespace SafeExamBrowser.Server
|
|||
|
||||
internal bool TryParseConnectionToken(HttpResponseMessage response, out string connectionToken)
|
||||
{
|
||||
connectionToken = default(string);
|
||||
connectionToken = default;
|
||||
|
||||
try
|
||||
{
|
||||
|
@ -108,7 +127,7 @@ namespace SafeExamBrowser.Server
|
|||
logger.Error("Failed to parse connection token!", e);
|
||||
}
|
||||
|
||||
return connectionToken != default(string);
|
||||
return connectionToken != default;
|
||||
}
|
||||
|
||||
internal bool TryParseExams(HttpContent content, out IList<Exam> exams)
|
||||
|
@ -141,8 +160,8 @@ namespace SafeExamBrowser.Server
|
|||
internal bool TryParseInstruction(HttpContent content, out Attributes attributes, out string instruction, out string instructionConfirmation)
|
||||
{
|
||||
attributes = new Attributes();
|
||||
instruction = default(string);
|
||||
instructionConfirmation = default(string);
|
||||
instruction = default;
|
||||
instructionConfirmation = default;
|
||||
|
||||
try
|
||||
{
|
||||
|
@ -170,13 +189,31 @@ namespace SafeExamBrowser.Server
|
|||
logger.Error("Failed to parse instruction!", e);
|
||||
}
|
||||
|
||||
return instruction != default(string);
|
||||
return instruction != default;
|
||||
}
|
||||
|
||||
internal bool TryParseOauth2Token(HttpContent content, out string oauth2Token)
|
||||
{
|
||||
oauth2Token = default;
|
||||
|
||||
try
|
||||
{
|
||||
var json = JsonConvert.DeserializeObject(Extract(content)) as JObject;
|
||||
|
||||
oauth2Token = json["access_token"].Value<string>();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Error("Failed to parse Oauth2 token!", e);
|
||||
}
|
||||
|
||||
return oauth2Token != default;
|
||||
}
|
||||
|
||||
private Attributes ParseProctoringAttributes(JObject attributesJson, string instruction)
|
||||
{
|
||||
var attributes = new Attributes();
|
||||
|
||||
|
||||
switch (instruction)
|
||||
{
|
||||
case Instructions.NOTIFICATION_CONFIRM:
|
||||
|
@ -261,24 +298,6 @@ namespace SafeExamBrowser.Server
|
|||
}
|
||||
}
|
||||
|
||||
internal bool TryParseOauth2Token(HttpContent content, out string oauth2Token)
|
||||
{
|
||||
oauth2Token = default(string);
|
||||
|
||||
try
|
||||
{
|
||||
var json = JsonConvert.DeserializeObject(Extract(content)) as JObject;
|
||||
|
||||
oauth2Token = json["access_token"].Value<string>();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Error("Failed to parse Oauth2 token!", e);
|
||||
}
|
||||
|
||||
return oauth2Token != default(string);
|
||||
}
|
||||
|
||||
private string Extract(HttpContent content)
|
||||
{
|
||||
var task = Task.Run(async () =>
|
||||
|
|
|
@ -10,6 +10,7 @@ using System;
|
|||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
|
@ -95,23 +96,7 @@ namespace SafeExamBrowser.Server
|
|||
if (success && parser.TryParseApi(response.Content, out api))
|
||||
{
|
||||
logger.Info("Successfully loaded server API.");
|
||||
|
||||
var secret = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{settings.ClientName}:{settings.ClientSecret}"));
|
||||
var authorization = ("Authorization", $"Basic {secret}");
|
||||
var content = "grant_type=client_credentials&scope=read write";
|
||||
var contentType = "application/x-www-form-urlencoded";
|
||||
|
||||
success = TryExecute(HttpMethod.Post, api.AccessTokenEndpoint, out response, content, contentType, authorization);
|
||||
message = response.ToLogString();
|
||||
|
||||
if (success && parser.TryParseOauth2Token(response.Content, out oauth2Token))
|
||||
{
|
||||
logger.Info("Successfully retrieved OAuth2 token.");
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error("Failed to retrieve OAuth2 token!");
|
||||
}
|
||||
success = TryRetrieveOAuth2Token(out message);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -543,6 +528,28 @@ namespace SafeExamBrowser.Server
|
|||
}
|
||||
}
|
||||
|
||||
private bool TryRetrieveOAuth2Token(out string message)
|
||||
{
|
||||
var secret = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{settings.ClientName}:{settings.ClientSecret}"));
|
||||
var authorization = ("Authorization", $"Basic {secret}");
|
||||
var content = "grant_type=client_credentials&scope=read write";
|
||||
var contentType = "application/x-www-form-urlencoded";
|
||||
var success = TryExecute(HttpMethod.Post, api.AccessTokenEndpoint, out var response, content, contentType, authorization);
|
||||
|
||||
message = response.ToLogString();
|
||||
|
||||
if (success && parser.TryParseOauth2Token(response.Content, out oauth2Token))
|
||||
{
|
||||
logger.Info("Successfully retrieved OAuth2 token.");
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error("Failed to retrieve OAuth2 token!");
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
private bool TryExecute(
|
||||
HttpMethod method,
|
||||
string url,
|
||||
|
@ -555,22 +562,7 @@ namespace SafeExamBrowser.Server
|
|||
|
||||
for (var attempt = 0; attempt < settings.RequestAttempts && (response == default || !response.IsSuccessStatusCode); attempt++)
|
||||
{
|
||||
var request = new HttpRequestMessage(method, url);
|
||||
|
||||
if (content != default)
|
||||
{
|
||||
request.Content = new StringContent(content, Encoding.UTF8);
|
||||
|
||||
if (contentType != default)
|
||||
{
|
||||
request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var (name, value) in headers)
|
||||
{
|
||||
request.Headers.Add(name, value);
|
||||
}
|
||||
var request = BuildRequest(method, url, content, contentType, headers);
|
||||
|
||||
try
|
||||
{
|
||||
|
@ -580,6 +572,16 @@ namespace SafeExamBrowser.Server
|
|||
{
|
||||
logger.Debug($"Completed request: {request.Method} '{request.RequestUri}' -> {response.ToLogString()}");
|
||||
}
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.Unauthorized && parser.IsTokenExpired(response.Content))
|
||||
{
|
||||
logger.Info("OAuth2 token has expired, attempting to retrieve new one...");
|
||||
|
||||
if (TryRetrieveOAuth2Token(out var message))
|
||||
{
|
||||
headers = UpdateOAuth2Token(headers);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
|
@ -594,5 +596,53 @@ namespace SafeExamBrowser.Server
|
|||
|
||||
return response != default && response.IsSuccessStatusCode;
|
||||
}
|
||||
|
||||
private HttpRequestMessage BuildRequest(
|
||||
HttpMethod method,
|
||||
string url,
|
||||
string content = default,
|
||||
string contentType = default,
|
||||
params (string name, string value)[] headers)
|
||||
{
|
||||
var request = new HttpRequestMessage(method, url);
|
||||
|
||||
if (content != default)
|
||||
{
|
||||
request.Content = new StringContent(content, Encoding.UTF8);
|
||||
|
||||
if (contentType != default)
|
||||
{
|
||||
request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType);
|
||||
}
|
||||
}
|
||||
|
||||
request.Headers.Add("Accept", "application/json, */*");
|
||||
|
||||
foreach (var (name, value) in headers)
|
||||
{
|
||||
request.Headers.Add(name, value);
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
private (string name, string value)[] UpdateOAuth2Token((string name, string value)[] headers)
|
||||
{
|
||||
var result = new List<(string name, string value)>();
|
||||
|
||||
foreach (var header in headers)
|
||||
{
|
||||
if (header.name == "Authorization")
|
||||
{
|
||||
result.Add(("Authorization", $"Bearer {oauth2Token}"));
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Add(header);
|
||||
}
|
||||
}
|
||||
|
||||
return result.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue