Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you provide some context as to why this was included in this PR?

"configurations": [
{
"name": ".NET Core Docker Attach",
"type": "coreclr",
"request": "attach",
"processId": "${command:pickRemoteProcess}",
"pipeTransport": {
"pipeProgram": "docker",
"pipeArgs": ["exec", "-i", "jasper-api-1"],
"debuggerPath": "/vsdbg/vsdbg",
"pipeCwd": "${workspaceRoot}",
"quoteArgs": false
},
"sourceFileMap": {
"/opt/app-root/src": "${workspaceRoot}/"
}
}
]
}
79 changes: 0 additions & 79 deletions api/Controllers/MockOrdersController.cs

This file was deleted.

16 changes: 13 additions & 3 deletions api/Documents/Strategies/OrderDocumentStrategy.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
using System;
using System.IO;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using CSOCommon.Clients.JudicialServices;
using CSOCommon.Models;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json.Serialization;
using Nutrient.NativeSDK.API.Exceptions;
Expand All @@ -18,7 +22,7 @@ public class OrderDocumentStrategy : IDocumentStrategy

public DocumentType Type => DocumentType.Order;

public OrderDocumentStrategy(IJudicialServicesClient judicialClient, IConfiguration configuration)
public OrderDocumentStrategy(IJudicialServicesClient judicialClient, IConfiguration configuration, ClaimsPrincipal currentUser)
{
_judicialClient = judicialClient;
_judicialClient.JsonSerializerSettings.ContractResolver = new SafeContractResolver { NamingStrategy = new CamelCaseNamingStrategy() };
Expand All @@ -37,10 +41,16 @@ public async Task<MemoryStream> Invoke(PdfDocumentRequestDetails documentRequest
throw new InvalidArgumentException("Invalid agency id");
}

var isValidDocumentId = double.TryParse(documentRequest.DocumentId, out var documentId);
if (string.IsNullOrWhiteSpace(documentRequest.DocumentId))
{
throw new InvalidArgumentException("Invalid document id.");
}

var decodedDocumentId = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(documentRequest.DocumentId));
var isValidDocumentId = double.TryParse(decodedDocumentId, out var documentId);
if (!isValidDocumentId)
{
throw new InvalidArgumentException("Invalid document id");
throw new InvalidArgumentException("Invalid document id.");
}

using var response = await _judicialClient.GetJudicialDocumentAsync(
Expand Down
111 changes: 109 additions & 2 deletions api/Infrastructure/Authentication/CsoBearerTokenHandler.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,41 @@
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using IdentityModel.Client;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Scv.Api.Infrastructure.Options;
using Scv.Core.Helpers.Extensions;

namespace Scv.Api.Infrastructure.Authentication
{
public sealed class CsoBearerTokenHandler(
public sealed partial class CsoBearerTokenHandler(
IKeycloakTokenService tokenService,
IOptions<CsoKeycloakClientOptions> options,
IHttpContextAccessor httpContextAccessor,
IHttpClientFactory httpClientFactory,
IConfiguration configuration,
ILogger<CsoBearerTokenHandler> logger) : DelegatingHandler
{
/// <summary>
/// Url pattern for retrieving CSO documents
/// </summary>
/// <returns>Returns true if Url path matches the CSO GetJudicialDocument endpoint pattern.</returns>
[GeneratedRegex(@"/judicial/[^/]+/document/?$", RegexOptions.IgnoreCase)]
private static partial Regex GetJudicialDocumentUrlRegex();

private readonly IKeycloakTokenService _tokenService = tokenService;
private readonly CsoKeycloakClientOptions _options = options.Value ?? throw new ArgumentNullException(nameof(options));
private readonly IHttpContextAccessor _httpContextAccessor = httpContextAccessor;
private readonly IHttpClientFactory _httpClientFactory = httpClientFactory;
private readonly IConfiguration _configuration = configuration;
private readonly ILogger<CsoBearerTokenHandler> _logger = logger;

protected override async Task<HttpResponseMessage> SendAsync(
Expand All @@ -26,15 +46,102 @@ protected override async Task<HttpResponseMessage> SendAsync(

if (request.Headers.Authorization == null)
{
var token = await _tokenService.GetServiceAccountTokenAsync(_options, cancellationToken);
string token;
if (IsGetJudicialDocumentRequest(request))
{
_logger.LogDebug("CSO GetJudicialDocument request detected; using current user's access token.");
token = await GetUserAccessTokenAsync(cancellationToken)
?? await _tokenService.GetServiceAccountTokenAsync(_options, cancellationToken);
}
else
{
token = await _tokenService.GetServiceAccountTokenAsync(_options, cancellationToken);
}

request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
}
else
{
_logger.LogDebug("CSO request already contains an Authorization header.");
}

_logger.LogInformation("Sending request to {RequestUri} with method {Method}", request.RequestUri, request.Method);

return await base.SendAsync(request, cancellationToken);
}

private static bool IsGetJudicialDocumentRequest(HttpRequestMessage request)
{
if (request.Method != HttpMethod.Get || request.RequestUri == null)
{
return false;
}

return GetJudicialDocumentUrlRegex().IsMatch(request.RequestUri.AbsolutePath);
}

/// <summary>
/// Retrieves the currently logged-on user's access token
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns>User access token</returns>
private async Task<string> GetUserAccessTokenAsync(CancellationToken cancellationToken)
{
var httpContext = _httpContextAccessor.HttpContext;
if (httpContext == null)
{
_logger.LogWarning("No HttpContext available; cannot obtain user access token.");
return null;
}

var refreshToken = await httpContext.GetTokenAsync(
CookieAuthenticationDefaults.AuthenticationScheme, "refresh_token");
if (string.IsNullOrWhiteSpace(refreshToken))
{
_logger.LogWarning("No refresh_token found for the current user; cannot obtain user access token.");
return null;
}

var client = _httpClientFactory.CreateClient(KeycloakTokenService.HttpClientName);
var response = await client.RequestRefreshTokenAsync(new RefreshTokenRequest
{
Address = _configuration.GetNonEmptyValue("Keycloak:Authority") + "/protocol/openid-connect/token",
ClientId = _configuration.GetNonEmptyValue("Keycloak:Client"),
ClientSecret = _configuration.GetNonEmptyValue("Keycloak:Secret"),
RefreshToken = refreshToken
}, cancellationToken);

if (response.IsError || string.IsNullOrWhiteSpace(response.AccessToken))
{
_logger.LogError(
"Failed to exchange user refresh_token for access_token. Error: {Error}. Description: {ErrorDescription}",
response.Error ?? "unknown", response.ErrorDescription);
return null;
}

try
{
// Persist the new refresh token and expiry time back to the auth cookie so that subsequent requests can use it.
var authResult = await httpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
if (authResult.Succeeded && authResult.Properties != null)
{
authResult.Properties.UpdateTokenValue("refresh_token", response.RefreshToken);
authResult.Properties.UpdateTokenValue(
"expires_at",
DateTimeOffset.UtcNow.AddSeconds(response.ExpiresIn)
.ToString("o", System.Globalization.CultureInfo.InvariantCulture));
await httpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
authResult.Principal,
authResult.Properties);
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to persist rotated refresh_token back to the auth cookie.");
}

return response.AccessToken;
}
}
}
Loading
Loading