Skip to content

Commit e694c87

Browse files
API: implement client id/secret access check for all endpoints
1 parent b8cfcdd commit e694c87

File tree

5 files changed

+127
-93
lines changed

5 files changed

+127
-93
lines changed

src/Certify.Server/Certify.Server.Hub.Api/Controllers/internal/ApiControllerBase.cs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ internal async Task<bool> IsAuthorized(ICertifyInternalApiClient internalApiClie
4242
return false;
4343
}
4444

45+
/// if check does not specify security principle use the current user
46+
if (check.SecurityPrincipleId == null)
47+
{
48+
check.SecurityPrincipleId = CurrentAuthContext.UserId;
49+
}
50+
4551
return await internalApiClient.CheckSecurityPrincipleHasAccess(check, CurrentAuthContext);
4652
}
4753

@@ -56,6 +62,47 @@ internal async Task<bool> IsAuthorized(ICertifyInternalApiClient internalApiClie
5662
{
5763
return await internalApiClient.CheckApiTokenHasAccess(token, check, CurrentAuthContext);
5864
}
65+
66+
internal async Task<Models.Config.ActionResult> CheckRequestAuthorized(ICertifyInternalApiClient internalApiClient, AccessCheck check)
67+
{
68+
// check for authorization bearer token first
69+
70+
var currenAuthContextCheckOK = await IsAuthorized(internalApiClient, check);
71+
72+
if (currenAuthContextCheckOK)
73+
{
74+
return new Models.Config.ActionResult("Authorized by bearer token", true);
75+
}
76+
77+
// check for access token in request headers
78+
var accessToken = GetAccessTokenFromRequest();
79+
80+
if (accessToken == null)
81+
{
82+
return new Models.Config.ActionResult("X-Client-ID or X-Client-Secret HTTP header missing in request", false);
83+
}
84+
85+
return await IsAccessTokenAuthorized(internalApiClient, accessToken, check);
86+
}
87+
88+
internal AccessToken? GetAccessTokenFromRequest()
89+
{
90+
var clientId = Request.Headers["X-Client-ID"];
91+
var secret = Request.Headers["X-Client-Secret"];
92+
93+
if (string.IsNullOrWhiteSpace(clientId) || string.IsNullOrWhiteSpace(secret))
94+
{
95+
return null;
96+
}
97+
else
98+
{
99+
return new AccessToken
100+
{
101+
ClientId = clientId,
102+
Secret = secret
103+
};
104+
}
105+
}
59106
/// <summary>
60107
/// Get the corresponding auth context to pass to the backend service
61108
/// </summary>

src/Certify.Server/Certify.Server.Hub.Api/Controllers/v1/CertificateController.cs

Lines changed: 13 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
using System.Net;
1+
using System.Net;
22
using System.Text;
3-
using System.Text.Unicode;
43
using Certify.Client;
54
using Certify.Models.Hub;
65
using Certify.Models.Reporting;
@@ -53,48 +52,11 @@ public CertificateController(ILogger<CertificateController> logger, ICertifyInte
5352
[ProducesResponseType(typeof(FileContentResult), 200)]
5453
public async Task<IActionResult> Download(string instanceId, string managedCertId, string format)
5554
{
56-
var accessPermitted = false;
55+
var accessCheck = await CheckRequestAuthorized(_client, new AccessCheck(default!, ResourceTypes.Certificate, StandardResourceActions.CertificateDownload));
5756

58-
if (CurrentAuthContext != null)
57+
if (!accessCheck.IsSuccess)
5958
{
60-
// auth based on JWT identity
61-
var authCheckOK = await IsAuthorized(_client, new AccessCheck(CurrentAuthContext.UserId, ResourceTypes.Certificate, StandardResourceActions.CertificateDownload));
62-
if (!authCheckOK)
63-
{
64-
return Problem(detail: "Identity not authorized for this action", statusCode: (int)HttpStatusCode.Unauthorized);
65-
}
66-
else
67-
{
68-
accessPermitted = true;
69-
}
70-
}
71-
else
72-
{
73-
// auth based on client id and client secret
74-
// check token and access control before allowing download
75-
var clientId = Request.Headers["X-Client-ID"];
76-
var secret = Request.Headers["X-Client-Secret"];
77-
78-
if (string.IsNullOrWhiteSpace(clientId) || string.IsNullOrWhiteSpace(secret))
79-
{
80-
return Problem(detail: "X-Client-ID or X-Client-Secret HTTP header missing in request", statusCode: (int)HttpStatusCode.Unauthorized);
81-
}
82-
83-
var accessPermittedResult = await IsAccessTokenAuthorized(_client, new AccessToken { ClientId = clientId, Secret = secret }, new AccessCheck(default!, ResourceTypes.Certificate, StandardResourceActions.CertificateDownload));
84-
85-
if (accessPermittedResult.IsSuccess)
86-
{
87-
accessPermitted = true;
88-
}
89-
else
90-
{
91-
return Problem(detail: accessPermittedResult.Message, statusCode: (int)HttpStatusCode.Unauthorized);
92-
}
93-
}
94-
95-
if (!accessPermitted)
96-
{
97-
return Unauthorized();
59+
return Problem(detail: accessCheck.Message, statusCode: (int)HttpStatusCode.Unauthorized);
9860
}
9961

10062
// default to PFX output
@@ -143,7 +105,15 @@ public async Task<IActionResult> Download(string instanceId, string managedCertI
143105
Response.Headers.Append("ETag", managedCert.CertificateThumbprintHash.ToLowerInvariant());
144106
}
145107

146-
return new FileContentResult(exportResult.Result, "application/x-pkcs12") { FileDownloadName = "certificate.pfx" };
108+
if (format == "pfx")
109+
{
110+
return new FileContentResult(exportResult.Result, "application/x-pkcs12") { FileDownloadName = "certificate.pfx" };
111+
}
112+
else
113+
{
114+
// for PEM formats, return as text/plain
115+
return new FileContentResult(exportResult.Result, "text/plain") { FileDownloadName = $"{format}.pem" };
116+
}
147117
}
148118
else
149119
{

src/Certify.Server/Certify.Server.Hub.Api/Controllers/v1/SystemController.cs

Lines changed: 22 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -105,15 +105,16 @@ public async Task<IActionResult> CheckJoining(bool? register = false)
105105

106106
// auth based on client id and client secret
107107
// check token and access control before allowing download
108-
var clientId = Request.Headers["X-Client-ID"];
109-
var secret = Request.Headers["X-Client-Secret"];
110-
var hubAssignedInstanceId = Request.Headers["X-Certify-HubAssignedId"];
111108

112-
if (string.IsNullOrWhiteSpace(clientId) || string.IsNullOrWhiteSpace(secret))
109+
var accessCheck = await CheckRequestAuthorized(_client, new AccessCheck(default!, ResourceTypes.ManagedInstance, StandardResourceActions.ManagementHubInstanceJoin));
110+
111+
if (!accessCheck.IsSuccess)
113112
{
114-
return Problem(detail: "X-Client-ID or X-Client-Secret HTTP header missing in request", statusCode: (int)HttpStatusCode.Unauthorized);
113+
return Problem(detail: accessCheck.Message, statusCode: (int)HttpStatusCode.Unauthorized);
115114
}
116115

116+
var hubAssignedInstanceId = Request.Headers["X-Certify-HubAssignedId"];
117+
117118
// if hub assigned instance id is provided we will either check the supplied hub assigned instance id or create a new one
118119

119120
// check if we know this instance, if so, check the supplied hub assigned instance ID
@@ -144,40 +145,32 @@ public async Task<IActionResult> CheckJoining(bool? register = false)
144145
return Problem(detail: "X-Certify-HubAssignedId HTTP header missing in request", statusCode: (int)HttpStatusCode.Unauthorized);
145146
}
146147

147-
var accessPermittedResult = await IsAccessTokenAuthorized(_client, new AccessToken { ClientId = clientId, Secret = secret }, new AccessCheck(default!, ResourceTypes.ManagedInstance, StandardResourceActions.ManagementHubInstanceJoin));
148+
var joiningInfo = new HubJoiningInfo();
148149

149-
if (accessPermittedResult.IsSuccess)
150-
{
151-
var joiningInfo = new HubJoiningInfo();
152-
153-
var versionInfo = await _client.GetAppVersion();
150+
var versionInfo = await _client.GetAppVersion();
154151

155-
joiningInfo.Version = new Models.Hub.VersionInfo
156-
{
157-
Version = versionInfo,
158-
Product = "Certify Management Hub",
159-
};
152+
joiningInfo.Version = new Models.Hub.VersionInfo
153+
{
154+
Version = versionInfo,
155+
Product = "Certify Management Hub",
156+
};
160157

161-
joiningInfo.HubEndpoint = "api/internal/managementhub";
162-
joiningInfo.Message = "Joining OK";
158+
joiningInfo.HubEndpoint = "api/internal/managementhub";
159+
joiningInfo.Message = "Joining OK";
163160

164-
var _config = HttpContext.RequestServices.GetRequiredService<IConfiguration>();
165-
var jwtService = new Hub.Api.Services.JwtService(_config);
161+
var _config = HttpContext.RequestServices.GetRequiredService<IConfiguration>();
162+
var jwtService = new Hub.Api.Services.JwtService(_config);
166163

167-
var additionalClaims = new List<Claim>
164+
var additionalClaims = new List<Claim>
168165
{
169166
new Claim("hub-assigned-id", Guid.NewGuid().ToString())
170167
};
171168

172-
joiningInfo.JoiningToken = jwtService.GenerateSecurityToken($"{clientId}", additionalClaims: additionalClaims);
173-
joiningInfo.HubAssignedInstanceId = hubAssignedInstanceId!;
169+
joiningInfo.JoiningToken = jwtService.GenerateSecurityToken($"{Request.Headers["X-Client-ID"]}", additionalClaims: additionalClaims);
170+
joiningInfo.HubAssignedInstanceId = hubAssignedInstanceId!;
171+
172+
return new OkObjectResult(joiningInfo);
174173

175-
return new OkObjectResult(joiningInfo);
176-
}
177-
else
178-
{
179-
return Problem(detail: accessPermittedResult.Message, statusCode: (int)HttpStatusCode.Unauthorized);
180-
}
181174
}
182175
}
183176
}

src/Certify.Server/Certify.Server.HubService/Program.cs

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
using System.Reflection;
2-
using System.Runtime.InteropServices;
1+
using System.Runtime.InteropServices;
32
using Certify.Client;
43
using Certify.Management;
54
using Certify.Models;
@@ -144,11 +143,8 @@
144143

145144
builder.Services.AddEndpointsApiExplorer();
146145

147-
// Register the Swagger generator, defining 1 or more Swagger documents
148-
// https://docs.microsoft.com/en-us/aspnet/core/tutorials/getting-started-with-swashbuckle?view=aspnetcore-3.1&tabs=visual-studio
149146
builder.Services.AddSwaggerGen(c =>
150147
{
151-
152148
// docs UI will be available at /docs
153149

154150
c.SwaggerDoc("v1", new OpenApiInfo
@@ -160,16 +156,6 @@
160156

161157
c.UseAllOfToExtendReferenceSchemas();
162158

163-
/* c.DocInclusionPredicate((docName, apiDesc) =>
164-
{
165-
if (!apiDesc.TryGetMethodInfo(out MethodInfo methodInfo))
166-
{
167-
return false;
168-
}
169-
170-
return methodInfo.DeclaringType.Namespace.StartsWith("Certify.Server.Hub.Api.Controllers");
171-
});*/
172-
173159
// use the actual method names as the generated operation id
174160
c.CustomOperationIds(e =>
175161
$"{e.ActionDescriptor.RouteValues["action"]}"
@@ -186,7 +172,23 @@
186172
Type = SecuritySchemeType.Http
187173
});
188174

189-
// set security requirement
175+
// add custom security definitions for client ID and client secret
176+
c.AddSecurityDefinition("X-Client-ID", new OpenApiSecurityScheme
177+
{
178+
Description = "Client ID header",
179+
Name = "X-Client-ID",
180+
In = ParameterLocation.Header,
181+
Type = SecuritySchemeType.ApiKey
182+
});
183+
c.AddSecurityDefinition("X-Client-Secret", new OpenApiSecurityScheme
184+
{
185+
Description = "Client Secret header",
186+
Name = "X-Client-Secret",
187+
In = ParameterLocation.Header,
188+
Type = SecuritySchemeType.ApiKey
189+
});
190+
191+
// Add security requirements: either Bearer OR (X-Client-ID AND X-Client-Secret)
190192
c.AddSecurityRequirement(new OpenApiSecurityRequirement
191193
{
192194
{
@@ -201,8 +203,26 @@
201203
}
202204
});
203205

206+
c.AddSecurityRequirement(new OpenApiSecurityRequirement
207+
{
208+
{
209+
new OpenApiSecurityScheme
210+
{
211+
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "X-Client-ID" }
212+
},
213+
new List<string>()
214+
},
215+
{
216+
new OpenApiSecurityScheme
217+
{
218+
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "X-Client-Secret" }
219+
},
220+
new List<string>()
221+
}
222+
});
223+
204224
// Set the comments path for the Swagger JSON and UI.
205-
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
225+
var xmlFile = $"Certify.Server.Hub.Api.xml";
206226
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
207227
c.IncludeXmlComments(xmlPath);
208228

src/Certify.SourceGenerators/PublicAPISourceGenerator.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,10 +146,14 @@ public partial class {config.PublicAPIController}Controller
146146
foreach (var perm in config.RequiredPermissions)
147147
{
148148
fragment += $@"
149-
if (!await IsAuthorized(_client, ""{perm.ResourceType}"" , ""{perm.Action}""))
150-
{{
151-
return Unauthorized();
152-
}}
149+
150+
var accessCheck = await CheckRequestAuthorized(_client, new AccessCheck(default!, ""{perm.ResourceType}"" ,""{perm.Action}""));
151+
152+
if (!accessCheck.IsSuccess)
153+
{{
154+
return Problem(detail: accessCheck.Message, statusCode: (int)System.Net.HttpStatusCode.Unauthorized);
155+
}}
156+
153157
";
154158
}
155159

0 commit comments

Comments
 (0)