# Technical documentation — findndrive_api_net

Evidence date: 2026-10-15. Source: `git@bitbucket.org:overdrive/findndrive_api_net.git`, `master` at `a2414dd` (2023-12-12). Secret values are omitted.

## Stack

| Item | Value | Evidence |
|---|---|---|
| Language | C# | `.csproj` files, `README.md` |
| Framework | `net6.0` on every project | `TargetFramework` |
| Host | ASP.NET Core Web SDK | `FindAndDrive.API.csproj` `Microsoft.NET.Sdk.Web` |
| README claim | .NET 6 | `README.md` |
| CI SDK image | `mcr.microsoft.com/dotnet/sdk:7.0` | `bitbucket-pipelines.yml` |
| API style | Controllers, `[ApiController]`, route `api/[controller]` | controllers |
| Auth | JWT bearer, symmetric HMAC-SHA256 | `Startup`, `AuthenticationService` |
| Docs | Swashbuckle 6.2.3 | API csproj |
| ORM | EF Core 6.0.13, SQL Server provider | API and Persistence csproj |
| JSON in middleware and gateways | Newtonsoft.Json 13.0.2 | API csproj. ASP.NET Core's own input formatter is the default MVC one (not explicitly switched to Newtonsoft) |
| Tests | xUnit 2.4.1, Moq 4.18.4, `Microsoft.AspNetCore.Mvc.Testing` 6.0.13, coverlet 3.1.2 | test csproj |
| Load | NBomber 4.1.0 | performance csproj |
| Nullable and implicit usings | enabled | all csproj files |

`Program.Main` builds the host, calls `Start`, then `WaitForShutdown`. The startup class is `Startup`.

## Repository layout

```text
FindAndDrive.sln
FindAndDrive.API/                  controllers, Startup, Program, middleware, Swagger filter, appsettings
FindAndDrive.Domain/               services, entities, DTOs, enums, gateways interfaces, settings, helpers
FindAndDrive.Persistence/          DefaultContext, repositories, EF configurations, migrations, SeedData
FindAndDrive.Gateway.Experian/     ExperianGateway.cs
FindAndDrive.Gateway.TransUnion/   TransUnionGateway.cs
FindAndDrive.Tests/                unit and integration tests
FindAndDrive.Tests.Performance/    NBomber console app
database/FindAndDrive.bak          SQL Server backup, about 5.0 MB, not opened
bitbucket-pipelines.yml
README.md
.gitignore                         Visual Studio template; ignores Properties/launchSettings.json
```

No Dockerfile, appspec, Terraform, or GitHub Actions workflow is in the tree. `launchSettings.json` is gitignored, so the committed Kestrel URLs are unknown. The performance harness assumes `https://localhost:5001`.

## Request pipeline

`Startup.Configure` order:

1. Developer exception page, only when the environment is Development.
2. Swagger and Swagger UI, every environment.
3. Routing.
4. `ExceptionMiddleware`.
5. HTTPS redirection.
6. Authentication, then authorisation.
7. Controller endpoints.
8. After the pipeline is built: apply migrations, sync roles, maybe create the `Admin` client. This runs during `Configure`, so a failed database connection prevents the process from finishing startup.

`HttpClient` instances for the two gateways are constructed once in `ConfigureServices` and captured by the DI lambdas. They are not created through `IHttpClientFactory`.

## Configuration

Files: `FindAndDrive.API/appsettings.json` and `appsettings.Development.json`. User secrets id on the API project: `307977c1-b0b3-420b-9659-e93c9675bd6c`. Whether a secrets store is populated on a developer machine is **Unknown**.

| Key | In `appsettings.json` | In Development file | Used by |
|---|---|---|---|
| `ConnectionStrings:DefaultConnection` | empty | empty | `UseSqlServer` |
| `Logging:LogLevel:Default` | Information | Information | host logging |
| `Logging:LogLevel:Microsoft.AspNetCore` | Warning | Warning | host logging |
| `AllowedHosts` | `*` | not set in Development file | host filter |
| `SecuritySettings:Key` | set (184 characters) | set (184 characters) | JWT sign and validate |
| `SecuritySettings:Issuer` | empty | empty | configured as `ValidIssuer`, validation off |
| `SecuritySettings:Audience` | empty | empty | configured as `ValidAudience`, validation off |
| `SecuritySettings:LifetimeInMinutes` | 60 | 60 | token expiry |
| `ExperianSettings:BaseUrl` | `https://localhost:4000/` | absent | `HttpClient.BaseAddress` only |
| `ExperianSettings:ExperianUsername` | set | absent | request body |
| `ExperianSettings:ExperianPassword` | set | absent | request body |
| `ExperianSettings:ExperianID` | integer, copied onto `ApplicantAuditLog.ExperianId` | absent | audit row |
| `TransUnionSettings:BaseUrl` | `https://localhost:4000/` | absent | `HttpClient.BaseAddress` only |
| `TransUnionSettings:TransUnionUsername` | set, JSON number, 6 digits, fits in `Int32` | absent | SOAP body and `ApplicantAuditLog.TransUnionId` (`int`) |
| `TransUnionSettings:TransUnionPassword` | set | absent | SOAP body |
| `TransUnionSettings:ApiKey` | set, 128 characters | absent | SOAP body |

The Development file does not replace Experian or TransUnion sections, so a Development host still reads those values from `appsettings.json`.

**Security note, values omitted.** Bureau username, password, and API key, and the JWT signing key, are committed in `appsettings.json`. The performance project commits two API key id and secret pairs in `Program.cs`. `Startup.CreateInitialClient` writes a newly generated secret to stdout. These should be reviewed for rotation and for removal from git. This document does not repeat them.

JWT options that the code comments say to change for production, and their current values:

| Option | Current | Comment in `Startup` |
|---|---|---|
| `RequireHttpsMetadata` | false | none |
| `SaveToken` | false | "NB set this to true, when going to prod." |
| `ValidateIssuerSigningKey` | true | none |
| `ValidateIssuer` | false | "NB set this to true, when going to prod." |
| `ValidateAudience` | false | "NB set this to true, when going to prod." |
| `ValidateLifetime` | default (not set) | none |

Clock skew is the framework default. **Unknown** which value a deployment actually runs, because appsettings can be replaced on the server and that file is not in git.

## HTTP surface

| Method and path | Auth | Swagger |
|---|---|---|
| `POST /api/Authentication/token` | anonymous | shown |
| `POST /api/Authentication/refresh` | any authenticated JWT | shown |
| `POST /api/Financing/Pre-Qualification` | permission `Financing` | shown |
| `POST /api/Financing/Prediction` | `Financing` | shown |
| `GET /api/Financing/GetApplicantById` | `Financing` | shown |
| `POST /api/Financing/Experian` | `Financing` | hidden |
| `POST /api/Financing/Transunion` | `Financing` | hidden |
| `POST /api/Client` | `Client` | hidden |
| `GET /api/Client` | `Client` | hidden |
| `GET /api/Client/{clientId}` | `Client` | hidden |
| `PATCH /api/Client` | `Client` | hidden |
| `POST /api/ApiKey` | `ApiKey` | hidden |
| `GET /api/ApiKey/{clientId}` | `ApiKey` | hidden |
| `POST /api/ApiKey/revoke` | `ApiKey` | hidden |
| `POST /api/ApiKey/reinstate` | `ApiKey` | hidden |
| `GET /api/Role` | `Role` | hidden |

Authorisation policies are built from the `Permissions` enum. A policy passes when any `Permissions` claim equals the policy name. An empty claim list fails.

Swagger adds 401 and 403 response entries on actions it considers authorised, via `SwaggerAuthorizationFilter`.

There is no CORS policy, rate limiter, health endpoint, or API version prefix beyond the controller names.

## External integrations

### Experian

`ExperianGateway.getAffordability` and `getAffordabilityIndependent`:

- HTTP POST, `application/json`.
- URL: `https://webservices-uat.compuscan.co.za:9443/Afford/DoAffordability`.
- Body: `ExperianRequest` after `pUsername` and `pPassword` are set from configuration.
- Non-success status becomes `GatewayErrorException` (HTTP 504).
- `Transaction_Completed == false` becomes `BadRequestException` (HTTP 400) including `Err_Code` and `Err_String`.
- `Ret_Data` is deserialised to `ExperianGatewayResponse` on the parsed path. The independent path returns the raw string.
- A parse failure is logged and rethrown as `BadRequestException` with message `Response from Experian couldnt be parsed`.

The hostname contains `uat`. Whether production should use a different host is **Requires Confirmation**. `ExperianSettings.BaseUrl` does not point at this host.

### TransUnion

`TransUnionGateway.DoCreditInfo` and `DoCreditInfoIndependent`:

- HTTP POST, `application/soap+xml`.
- URL: `https://autotest.transunion.co.za/TUQuickCreditAndFraudv2/TransUnionQuickCrtAndFrd.asmx`.
- Body: SOAP 1.2 envelope calling `doQuickCreditAndFraud` in namespace `http://tempuri.org/`.
- The parsed path reads `//ns:doQuickCreditAndFraudResult`, treats the inner text as a JSON array, and maps keys `CreditScore`, `ConsumerInfo`, `LastAddress`, and `AuditLog`.
- Empty score `-`, or missing address, consumer, credit score, or audit log, becomes HTTP 400 on the parsed path.
- XML or JSON parse failures become `GatewayErrorException` (HTTP 504) and include the exception message.
- The independent path returns the raw HTTP body after the status check.

The hostname contains `autotest`. `TransUnionSettings.BaseUrl` does not point at this host.

Credentials travel inside the SOAP XML, not as an HTTP header.

## Data model

`DefaultContext` applies configurations from the Persistence assembly.

| Entity | Table | Relationship |
|---|---|---|
| `Client` | `Clients` | one-to-many API keys and applicants |
| `Role` | `Roles` | one-to-many API keys. `Permissions` stored as a string via `EnumConverter` |
| `ApiKey` | `ApiKeys` | FK to client and role. `KeySecretHash`, `Revoked` |
| `Applicant` | `Applicants` | FK `ClientId`. One-to-one finance, credit, fraud alert, audit log, prediction |
| `ApplicantFinance` | `ApplicantFinance` | amounts, deposit, Experian calculated fields, `PredictorConfidence` |
| `ApplicantCredit` | `ApplicantCredit` | score, indicator, band, reason |
| `ApplicantFraudAlert` | `ApplicantFraudAlert` | disparity, rating (always 1), market name |
| `ApplicantAuditLog` | `ApplicantAuditLog` | Experian id, TransUnion username, calling API key id (`FindAndDriveId`), timestamp, raw audit text |
| `ApplicantPredicition` | `ApplicantPredicition` | chance, estimated amounts, suggestion text. Type name is spelled Predicition |

`ApplicantRepository.Update` copies scalar applicant fields onto the tracked row. It does not no-op when the id is missing. The caller does not check the missing case.

Repositories are scoped per request. `GenericRepository` exists beside the specific repositories.

Migrations, oldest first:

1. `20230206200652_InitialMigration`
2. `20230210100858_EntityConfigurations`
3. `20230213123205_AddedApplicantEntities`
4. `20230214124803_addClientForeignKey`
5. `20230214130128_AddedMaxLengthToEnums`
6. `20230223124315_PreMergeIntoDeev`
7. `20230224112032_AddedNullableModifiedAt`
8. `20230512091302_AddedMonthlyAndTotalCosts`
9. `20230512105651_FindAndDriveId`
10. `20230519122702_ChancesOfApprovalEnum`
11. `20230519133824_AddingCorrectTypes`
12. `20230519141354_reasonVerbiageLongerLength`
13. `20230522114429_AddedAuditLogToApplicantAuditLog`
14. `20230522150458_IncreasedLengthOfChancesOfApproval`
15. `20230522221434_ReasonVerbiageLengthChange`
16. `20230525083717_UpdatedGenderMSForApplicant`
17. `20230525092856_Kappa`

`Startup` calls `Database.Migrate()` on every process start, then upserts roles by `RoleName`. Existing roles keep their ids and receive the seed permission list and a new `ModifiedAt`.

## Auth implementation

`ApiKeyHelper.GenerateApiKey` uses `RNGCryptoServiceProvider` (obsolete in later .NET, present here on net6.0) for 32 bytes.

`HashString` is SHA-256, UTF-8, `Convert.ToHexString` (uppercase hex). Comparison with the stored hash is ordinal string equality. A lowercase hex hash would not match.

Token claims are custom names `ClientId`, `apiKeyId`, and `Permissions`. They are not `ClaimTypes` URIs. Role name is not a claim.

Refresh parses the JWT with `JwtSecurityTokenHandler.ReadToken` without validating the signature again. The endpoint is `[Authorize]`, so the bearer middleware has already validated the token that arrived. The handler then re-signs those claims.

## Background work, queues, storage, monitoring

None. No `IHostedService`, Hangfire, queue client, blob client, or application-insights package is referenced.

Logging is `ILogger` plus `AddLogging()`. Experian parse errors are logged. `ExceptionMiddleware` logs the exception and the path. TransUnion gateway has no logger injected.

The Bitbucket pipeline is the only monitoring-related automation, and it is static analysis (SonarQube), not runtime monitoring.

## Security observations from the tree

These are facts about the code, not a pentest.

- Swagger is exposed outside Development.
- JWT issuer and audience checks are off. HTTPS metadata check is off.
- Signing key and bureau secrets are in the committed appsettings file.
- Performance source contains live-looking API secrets.
- TransUnion credentials are placed in the SOAP body.
- Gateway hosts are UAT and autotest hostnames.
- Hidden admin routes are still callable by any token with the matching permission.
- `CreateInitialClient` prints a secret to standard output.
- 500 responses hide exception text, except TransUnion parse failures on the 504 path.
- No rate limit and no lockout on `POST /api/Authentication/token`.
- Applicant personal data (id number, address, credit score, bureau audit text) is stored in SQL Server. The `.bak` in git was not inspected and may contain the same classes of data.

## Build, test, and deploy

Build: `dotnet build` on the solution, as the pipeline does. The pipeline does not pass a solution path explicitly; `dotnet build` with no argument builds the project or solution in the working directory. A solution file is at the root, so that is the likely target. **Inferred** from the CLI default.

Tests: `dotnet test` is not in the pipeline. Projects are `FindAndDrive.Tests` and `FindAndDrive.Tests.Performance`. Integration tests start the real `Startup`, which migrates `DefaultConnection`. With the committed empty connection string they cannot reach SQL Server unless the environment supplies one.

Deploy: not described beyond the README sentence that the deployed `appsettings.json` must contain the connection string. No publish profile is committed (`*.pubxml` is gitignored).

Sonar project key: `overdrive_findndrive_api_net_AYvUygomT-jCdK_QYjRL`. `SONAR_TOKEN` and `SONAR_HOST_URL` are pipeline variables, not in git.

## Git history on this remote

| Commit | Date | Subject |
|---|---|---|
| `1f682e8` | 2023-11-15 | Initial commit |
| `46736da` | 2023-11-15 | First commit |
| `d8d781d` | 2023-11-15 | bitbucket-pipelines.yml created online with Bitbucket |
| `a2414dd` | 2023-12-12 | README.md edited online with Bitbucket |

Migration dates in February–May 2023 predate this history. The application was imported in `First commit` rather than developed commit-by-commit on this remote. README's GitHub duplicate is a possible home for the earlier history. The URL is **Unknown**.

## Local notes for a reader

To run the API, a SQL Server connection string has to be supplied. The repository does not contain one. Startup will migrate and seed as soon as the host builds. The first console lines may include a new Admin API secret if no client is named `Admin`.

`Properties/launchSettings.json` is not in git. Ports are whatever the local file or `ASPNETCORE_URLS` sets.
