Skip to content

Commit b80aafc

Browse files
committed
ADR-0001 Axis Camera Authentication Negotiation
1 parent 40e546a commit b80aafc

6 files changed

Lines changed: 163 additions & 27 deletions

File tree

AxisIPCamera/AxisIPCamera.csproj

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@
2121

2222
<PackageReference Include="RestSharp" Version="112.1.0" />
2323

24-
<PackageReference Include="System.Formats.Asn1" Version="8.0.1" />
25-
2624
<None Update="manifest.json">
2725
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
2826
</None>

AxisIPCamera/Client/AxisHttpClient.cs

Lines changed: 79 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@
1717
using Microsoft.Extensions.Logging;
1818
using Newtonsoft.Json;
1919
using RestSharp;
20-
using RestSharp.Authenticators;
2120

2221
using Keyfactor.Logging;
2322
using Keyfactor.Orchestrators.Extensions;
24-
using Keyfactor.Extensions.Orchestrator.AxisIPCamera.Model;
2523
using Keyfactor.Orchestrators.Extensions.Interfaces;
24+
using Keyfactor.Extensions.Orchestrator.AxisIPCamera.Exceptions;
25+
using Keyfactor.Extensions.Orchestrator.AxisIPCamera.Model;
2626
using Keyfactor.Extensions.Orchestrator.AxisIPCamera.Helpers;
2727

2828
/* AxisHttpClient.cs
@@ -67,64 +67,118 @@ public AxisHttpClient(JobConfiguration config, CertificateStore store, IPAMSecre
6767
try
6868
{
6969
var errorContext = new CertificateErrorContext();
70-
70+
7171
Logger = LogHandler.GetClassLogger<AxisHttpClient>();
7272
Logger.LogTrace("Entered AxisHttpClient constructor.");
7373
Logger.LogTrace("Initializing Axis IP Camera HTTP client");
74-
74+
7575
// ** NOTE: Ignoring the default config.UseSSL custom field --- we will always connect to the device via HTTPS
7676
var baseRestClientUrl = $"https://{store.ClientMachine}";
77-
77+
7878
Logger.LogDebug($"Base HTTP client URL: {baseRestClientUrl}");
7979

80-
// Initialize custom HTTP handler to validate device identity
81-
RestClientOptions options = null;
82-
Logger.LogTrace($"Adding custom TLS cert validator to the HTTP client options...");
80+
// Retrieve username and password credentials to connect to the device
81+
Logger.LogTrace("Adding device credentials to the HTTP client options...");
82+
string username = PAMUtilities.ResolvePAMField(resolver, Logger, "API Username", config.ServerUsername);
83+
string password = PAMUtilities.ResolvePAMField(resolver, Logger, "API Password", config.ServerPassword);
84+
85+
// See ADR-0001 Axis Camera Authentication Negotiation
86+
// https://keyfactor.atlassian.net/wiki/spaces/IoTStrategy/pages/2968584197/ADR-0001+Axis+Camera+Authentication+Negotiation
87+
//
88+
// The client intentionally uses HttpClientHandler credentials
89+
// rather than RestSharp's HttpBasicAuthenticator to allow
90+
// automatic negotiation of Basic vs Digest authentication
91+
// based on the authentication challenge presented by the camera.
92+
Logger.LogInformation($"Adding custom TLS cert validator to the HTTP client options.");
93+
Logger.LogInformation($"Using HttpClientHandler credential negotiation for camera authentication.");
8394
var handler = new HttpClientHandler
8495
{
8596
ServerCertificateCustomValidationCallback =
86-
DeviceCertValidator.GetValidator(store.StorePath, errorContext, Logger)
97+
DeviceCertValidator.GetValidator(
98+
store.StorePath,
99+
errorContext,
100+
Logger),
101+
102+
Credentials = new NetworkCredential(username, password),
103+
104+
PreAuthenticate =
105+
false // PreAuthenticate is set to false to avoid the default behavior of sending the username and password in the Authorization header
87106
};
107+
// End ADR-0001
88108

89109
// Initialize HTTP client options with the base URL and custom TLS cert validator
90-
options = new RestClientOptions(baseRestClientUrl)
110+
RestClientOptions options = new RestClientOptions(baseRestClientUrl)
91111
{
92112
ConfigureMessageHandler = _ => handler
93113
};
94114

95-
// Add Basic Auth username and password credentials
96-
Logger.LogTrace("Adding Basic Auth Credentials to the HTTP client options...");
97-
string username = PAMUtilities.ResolvePAMField(resolver, Logger, "API Username", config.ServerUsername);
98-
string password = PAMUtilities.ResolvePAMField(resolver, Logger, "API Password", config.ServerPassword);
99-
100-
options.Authenticator = new HttpBasicAuthenticator(username, password);
101-
102115
// Add SSL validation
103116
Logger.LogTrace("Validating connection to the device...");
104117

105118
_httpClient = new RestClient(options);
106-
var request = new RestRequest("/"); // Initiates the TLS handshake to retrieve the server cert
119+
// Initiates the TLS handshake to verify the ability to authenticate to the camera
120+
var request = new RestRequest("axis-cgi/param.cgi?action=list&group=Network.HTTP.AuthenticationPolicy");
107121
var response = _httpClient.Execute(request);
108122

109-
// Build the list of errors to log to the console
123+
Logger.LogTrace($"Connection to the device response status code: {response.StatusCode}");
124+
125+
// Build the list of SSL certificate errors and log to the console
110126
StringBuilder errorSb = new StringBuilder();
111127
if (errorContext.HasErrors)
112128
{
113129
foreach (var error in errorContext.Errors)
114130
{
115131
errorSb.AppendLine(error);
116132
}
117-
throw new Exception(errorSb.ToString());
133+
134+
throw new DeviceCertValidationException(
135+
$"Device TLS cert validator errors encountered --- {errorSb}");
136+
}
137+
138+
// Begin ADR-0001 Axis Camera Authentication Negotiation
139+
// Log the WWW-Authenticate headers if 401 Unauthorized returned
140+
// Throw exception if connection cannot be made successfully to the camera
141+
if (response.StatusCode == HttpStatusCode.Unauthorized)
142+
{
143+
Logger.LogWarning("Camera returned 401 Unauthorized");
144+
145+
foreach (var header in response.Headers)
146+
{
147+
Logger.LogDebug($"WWW-Authenticate header: {header.Value}");
148+
}
149+
150+
throw new AuthenticationException(
151+
"Authentication to the Axis camera failed due to 401 Unauthorized. Verify the configured credentials.");
118152
}
119-
120-
Logger.LogTrace($"Connection to the device response status code: {response.StatusCode}");
153+
154+
if (!response.IsSuccessful)
155+
{
156+
throw new Exception(response.ErrorMessage);
157+
}
158+
// End ADR-0001
159+
121160
Logger.LogTrace("Completed Initialization of Axis IP Camera HTTP Client");
122161
Logger.LogTrace("Leaving AxisHttpClient constructor.");
123162
}
124-
catch (Exception e)
163+
catch (DeviceCertValidationException ex)
164+
{
165+
Logger.LogError("Device TLS cert validation failed while connecting to the device: " + LogHandler.FlattenException(ex));
166+
throw new Exception(ex.Message);
167+
}
168+
catch (AuthenticationException ex1)
169+
{
170+
Logger.LogError("Authentication to the device failed: " + LogHandler.FlattenException(ex1));
171+
throw new Exception(ex1.Message);
172+
}
173+
catch (HttpRequestException ex2)
174+
{
175+
Logger.LogError("Failed to communicate with the device: " + LogHandler.FlattenException(ex2));
176+
throw new Exception(ex2.Message);
177+
}
178+
catch (Exception ex3)
125179
{
126-
Logger.LogError("Error initializing Axis IP Camera HTTP Client: " + LogHandler.FlattenException(e));
127-
throw new Exception($"Device identity could not be verified successfully --- {e.Message}");
180+
Logger.LogError("Unexpected error while connecting to the device: " + LogHandler.FlattenException(ex3));
181+
throw new Exception(ex3.Message);
128182
}
129183
}
130184

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright 2026 Keyfactor
2+
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
3+
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
4+
// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS,
5+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions
6+
// and limitations under the License.
7+
8+
using System;
9+
10+
namespace Keyfactor.Extensions.Orchestrator.AxisIPCamera.Exceptions
11+
{
12+
public class AuthenticationException : Exception
13+
{
14+
public AuthenticationException(string message)
15+
: base(message)
16+
{
17+
}
18+
19+
public AuthenticationException(
20+
string message,
21+
Exception innerException)
22+
: base(message, innerException)
23+
{
24+
}
25+
}
26+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright 2026 Keyfactor
2+
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
3+
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
4+
// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS,
5+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions
6+
// and limitations under the License.
7+
8+
using System;
9+
10+
namespace Keyfactor.Extensions.Orchestrator.AxisIPCamera.Exceptions
11+
{
12+
public class DeviceCertValidationException : Exception
13+
{
14+
public DeviceCertValidationException(string message)
15+
: base(message)
16+
{
17+
}
18+
19+
public DeviceCertValidationException(
20+
string message,
21+
Exception innerException)
22+
: base(message, innerException)
23+
{
24+
}
25+
}
26+
}

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
v1.1.0
2+
- fix(logging): Improved exception handling to be more robust for HTTP client initialization
3+
- fix(auth): Implemented .NET native credential negotiation to support both Basic & Digest auth policies
24
- chore(docs): Updated doctool release actions to v5
35
- chore(build): Updated .NET target frameworks to net8.0 and net10.0
46
- fix(odkg): Fixed incorrect mapping of ECDSA algorithm to ECP. Updated to ECDSA.

docsource/content.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,19 @@ recommended to create a new account specifically for executing API calls. This a
7272
privileges since the orchestrator extension is capable of making configuration changes, such as installing and removing certificates.
7373
2. Currently supports AXIS M2035-LE Bullet Camera, AXIS OS version 12.2.62. Has not been tested with any other firmware version.
7474

75+
### Authentication
76+
77+
The Axis IP Camera Orchestrator Extension uses .NET HttpClientHandler credential negotiation when connecting to Axis devices over HTTPS.
78+
This allows the orchestrator to automatically negotiate the authentication mechanism required by the camera.
79+
The orchestrator has been validated against Axis cameras configured with:
80+
81+
- Basic
82+
- Digest
83+
- Basic & Digest
84+
- Recommended
85+
86+
As a result, customer-side changes to camera authentication policies are generally not required.
87+
7588
## Post Installation
7689

7790
The AXIS IP Camera Orchestrator Extension *always* connects to an AXIS IP Network Camera via HTTPS, regardless
@@ -110,6 +123,23 @@ These values must match or the session will be denied.
110123
> Therefore, you will need to install the full CA chain - including root and intermediate certificates - into the orchestrator server's local
111124
> certificate store.
112125
126+
## Troubleshooting
127+
128+
### 401 Unauthorized Responses
129+
130+
The Axis IP Camera Orchestrator Extension supports authentication negotiation and has been validated against Axis cameras configured for:
131+
132+
- Basic
133+
- Digest
134+
- Basic & Digest
135+
- Recommended
136+
137+
If a 401 Unauthorized response is encountered:
138+
139+
1. Verify the configured credentials.
140+
2. Verify connectivity to the device.
141+
3. Review orchestrator logs for authentication-related messages.
142+
113143
## Operational Notes
114144

115145
### AXIS OS 12 Firmware Recommendation

0 commit comments

Comments
 (0)