Skip to content

Commit

Permalink
Better authorization for Management API using JWT tokens.
Browse files Browse the repository at this point in the history
  • Loading branch information
sevensolutions committed Oct 6, 2024
1 parent 153c78d commit ff4493e
Show file tree
Hide file tree
Showing 6 changed files with 113 additions and 13 deletions.
9 changes: 8 additions & 1 deletion examples/agent.dev.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,15 @@

log_level = "TRACE"

# Example JWT Token:
# eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InN0YXRpYyJ9.eyJpc3MiOiJOb21hZElJUyIsImF1ZCI6Ik1hbmFnZW1lbnRBcGkiLCJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjk5OTk5OTk5OTksImpvYiI6WyIqIl0sImFsbG9jSWQiOlsiKiJdfQ.8MZL54z4pBw9pFk3jP4Yqy7kuKLgjeEXdaEdWI6GmgM

plugin "nomad_iis" {
args = ["--management-api-port=5004", "--management-api-key=12345"]
args = [
"--management-api-port=5004",
"--management-api-key=12345",
"--management-api-jwt-secret=VETkEPWkaVTxWf7J4Mm20KJWOx2cK4S7VvoP3ybjh6fr9P9PXvyhlY8HV2Jgxm2O"
]
config {
enabled = true,
fingerprint_interval = "10s"
Expand Down
58 changes: 47 additions & 11 deletions src/NomadIIS/ManagementApi/Authentication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using NomadIIS.ManagementApi;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks;

Expand All @@ -19,6 +22,7 @@ public sealed class ApiKeyAuthenticationOptions : AuthenticationSchemeOptions
{
public string HeaderName { get; set; } = "X-Api-Key";
public string? ApiKey { get; set; }
public string? ApiJwtSecret { get; set; }
}

public sealed class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationOptions>
Expand All @@ -28,28 +32,60 @@ public ApiKeyAuthenticationHandler ( IOptionsMonitor<ApiKeyAuthenticationOptions
{
}

protected override Task<AuthenticateResult> HandleAuthenticateAsync ()
protected override async Task<AuthenticateResult> HandleAuthenticateAsync ()
{
if ( string.IsNullOrEmpty( Options.ApiKey ) )
if ( string.IsNullOrEmpty( Options.ApiKey ) && string.IsNullOrEmpty( Options.ApiJwtSecret ) )
{
return Task.FromResult(
AuthenticateResult.Success(
new AuthenticationTicket( new ClaimsPrincipal( new ClaimsIdentity( Scheme.Name ) ), Scheme.Name ) ) );
return AuthenticateResult.Success(
new AuthenticationTicket( new ClaimsPrincipal( new ClaimsIdentity( Scheme.Name ) ), Scheme.Name ) );
}

if ( !Request.Headers.ContainsKey( Options.HeaderName ) )
return Task.FromResult( AuthenticateResult.Fail( $"Missing header {Options.HeaderName}." ) );
return AuthenticateResult.Fail( $"Missing header {Options.HeaderName}." );

string headerValue = Request.Headers[Options.HeaderName]!;

if ( headerValue != Options.ApiKey )
return Task.FromResult( AuthenticateResult.Fail( "Invalid token." ) );
if ( !string.IsNullOrEmpty( Options.ApiKey ) && headerValue == Options.ApiKey )
{
var principal = new ClaimsPrincipal( new ClaimsIdentity( Scheme.Name ) );

var ticket = new AuthenticationTicket( principal, Scheme.Name );

return AuthenticateResult.Success( ticket );
}

if ( !string.IsNullOrEmpty( Options.ApiJwtSecret ) )
{
var validationResult = await new JwtSecurityTokenHandler()
.ValidateTokenAsync( headerValue, new TokenValidationParameters()
{
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes( Options.ApiJwtSecret ) )
{
KeyId = "static"
},
ValidIssuer = "NomadIIS",
ValidateIssuer = true,
ValidAudience = "ManagementApi",
ValidateAudience = true
} );

if ( !validationResult.IsValid )
{
Logger.LogWarning( validationResult.Exception, "Received invalid JWT token for Management API." );

return AuthenticateResult.Fail( "Invalid token." );
}

var principal = new ClaimsPrincipal( new ClaimsIdentity( Scheme.Name ) );
var principal = new ClaimsPrincipal( validationResult.ClaimsIdentity );

var ticket = new AuthenticationTicket( principal, Scheme.Name );
var ticket = new AuthenticationTicket( principal, Scheme.Name );

return AuthenticateResult.Success( ticket );
}

return Task.FromResult( AuthenticateResult.Success( ticket ) );
return AuthenticateResult.Fail( "Invalid token." );
}
}
}
Expand Down
23 changes: 23 additions & 0 deletions src/NomadIIS/ManagementApi/Authorization.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#if MANAGEMENT_API
using Microsoft.AspNetCore.Http;
using NomadIIS.Services;
using System;

namespace NomadIIS.ManagementApi;

public static class Authorization
{
public static bool IsAuthorized ( this IisTaskHandle taskHandle, HttpContext httpContext )
{
var jobName = taskHandle.TaskConfig?.JobName ?? throw new InvalidOperationException();
var allocId = taskHandle.TaskConfig?.AllocId ?? throw new InvalidOperationException();

if ( !httpContext.User.HasClaim( "job", jobName ) && !httpContext.User.HasClaim( "job", "*" ) )
return false;
if ( !httpContext.User.HasClaim( "allocId", allocId ) && !httpContext.User.HasClaim( "allocId", "*" ) )
return false;

return true;
}
}
#endif
30 changes: 30 additions & 0 deletions src/NomadIIS/ManagementApi/ManagementApiController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ public async Task<IActionResult> GetStatus ( string allocId, string taskName )
if ( taskHandle is null )
return NotFound();

if ( !taskHandle.IsAuthorized( HttpContext ) )
return Forbid();

var status = await taskHandle.GetStatusAsync();

return Ok( status );
Expand All @@ -41,6 +44,9 @@ public async Task<IActionResult> StartAppPool ( string allocId, string taskName
if ( taskHandle is null )
return NotFound();

if ( !taskHandle.IsAuthorized( HttpContext ) )
return Forbid();

await taskHandle.StartAppPoolAsync();

return Ok();
Expand All @@ -53,6 +59,9 @@ public async Task<IActionResult> StopAppPool ( string allocId, string taskName )
if ( taskHandle is null )
return NotFound();

if ( !taskHandle.IsAuthorized( HttpContext ) )
return Forbid();

await taskHandle.StopAppPoolAsync();

return Ok();
Expand All @@ -65,6 +74,9 @@ public async Task<IActionResult> RecycleAppPool ( string allocId, string taskNam
if ( taskHandle is null )
return NotFound();

if ( !taskHandle.IsAuthorized( HttpContext ) )
return Forbid();

await taskHandle.RecycleAppPoolAsync();

return Ok();
Expand All @@ -78,6 +90,9 @@ public async Task<IActionResult> GetFileAsync ( string allocId, string taskName,
if ( taskHandle is null )
return NotFound();

if ( !taskHandle.IsAuthorized( HttpContext ) )
return Forbid();

path = HttpUtility.UrlDecode( path );

await taskHandle.DownloadFileAsync( HttpContext.Response, path );
Expand All @@ -92,6 +107,9 @@ public async Task<IActionResult> PutFileAsync ( string allocId, string taskName,
if ( taskHandle is null )
return NotFound();

if ( !taskHandle.IsAuthorized( HttpContext ) )
return Forbid();

path = HttpUtility.UrlDecode( path );

var isZip = HttpContext.Request.ContentType == "application/zip";
Expand All @@ -108,6 +126,9 @@ public async Task<IActionResult> PatchFileAsync ( string allocId, string taskNam
if ( taskHandle is null )
return NotFound();

if ( !taskHandle.IsAuthorized( HttpContext ) )
return Forbid();

path = HttpUtility.UrlDecode( path );

var isZip = HttpContext.Request.ContentType == "application/zip";
Expand All @@ -124,6 +145,9 @@ public async Task<IActionResult> DeleteFileAsync ( string allocId, string taskNa
if ( taskHandle is null )
return NotFound();

if ( !taskHandle.IsAuthorized( HttpContext ) )
return Forbid();

path = HttpUtility.UrlDecode( path );

await taskHandle.DeleteFileAsync( path );
Expand All @@ -140,6 +164,9 @@ public async Task<IActionResult> DeleteFileAsync ( string allocId, string taskNa
if ( taskHandle is null )
return NotFound();

if ( !taskHandle.IsAuthorized( HttpContext ) )
return Forbid();

var screenshot = await taskHandle.TakeScreenshotAsync( path, cancellationToken );

if ( screenshot is null )
Expand All @@ -156,6 +183,9 @@ public async Task<IActionResult> DeleteFileAsync ( string allocId, string taskNa
if ( taskHandle is null )
return NotFound();

if ( !taskHandle.IsAuthorized( HttpContext ) )
return Forbid();

var dumpFile = new FileInfo( Path.GetTempFileName() + ".dmp" );

try
Expand Down
1 change: 1 addition & 0 deletions src/NomadIIS/NomadIIS.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
</ItemGroup>

<ItemGroup Condition="$(DefineConstants.Contains('MANAGEMENT_API'))">
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.1.1" />
<PackageReference Include="Microsoft.Playwright" Version="1.47.0" />
<PackageReference Include="CliWrap" Version="3.6.6" />
</ItemGroup>
Expand Down
5 changes: 4 additions & 1 deletion src/NomadIIS/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@

#if MANAGEMENT_API
var mgmtApiKey = builder.Configuration.GetValue<string>( "management-api-key" );
var mgmtApiJwtSecret = builder.Configuration.GetValue<string>( "management-api-jwt-secret" );
var needsAuthorization = !string.IsNullOrEmpty( mgmtApiKey ) || !string.IsNullOrEmpty( mgmtApiJwtSecret );

if ( managementApiPort > 0 )
{
Expand All @@ -102,6 +104,7 @@
.AddApiKey( config =>
{
config.ApiKey = mgmtApiKey;
config.ApiJwtSecret = mgmtApiJwtSecret;
} );

builder.Services.AddAuthorization();
Expand Down Expand Up @@ -141,7 +144,7 @@
.MapControllers()
.RequireHost( $"*:{managementApiPort}" );

if ( !string.IsNullOrEmpty( mgmtApiKey ) )
if ( needsAuthorization )
epApi.RequireAuthorization();
}
#endif
Expand Down

0 comments on commit ff4493e

Please sign in to comment.