Skip to content

Commit e80b031

Browse files
committed
Return the auth challenge in the WWW-Authenticate header on authentication failure
1 parent b46feda commit e80b031

File tree

8 files changed

+133
-8
lines changed

8 files changed

+133
-8
lines changed

src/Ocelot/Authentication/Middleware/AuthenticationMiddleware.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,15 @@ public async Task Invoke(HttpContext httpContext)
4343

4444
Logger.LogWarning($"Client has NOT been authenticated for {httpContext.Request.Path} and pipeline error set. {error}");
4545

46+
// Perform a challenge. This populates the WWW-Authenticate header on the response
47+
await httpContext.ChallengeAsync(downstreamRoute.AuthenticationOptions.AuthenticationProviderKey);
48+
49+
// Since the response gets re-created down the pipeline, we store the challenge in the Items, so we can re-apply it when sending the response
50+
if (httpContext.Response.Headers.TryGetValue("WWW-Authenticate", out var authenticateHeader))
51+
{
52+
httpContext.Items.SetAuthChallenge(authenticateHeader);
53+
}
54+
4655
httpContext.Items.SetError(error);
4756
}
4857
}

src/Ocelot/Middleware/HttpItemsExtensions.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ public static void SetError(this IDictionary<object, object> input, Error error)
4545
input.Upsert("Errors", errors);
4646
}
4747

48+
public static void SetAuthChallenge(this IDictionary<object, object> input, string challengeString) =>
49+
input.Upsert("AuthChallenge", challengeString);
50+
51+
public static string AuthChallenge(this IDictionary<object, object> input) =>
52+
input.Get<string>("AuthChallenge");
53+
4854
public static void SetIInternalConfiguration(this IDictionary<object, object> input, IInternalConfiguration config)
4955
{
5056
input.Upsert("IInternalConfiguration", config);

src/Ocelot/Multiplexer/MultiplexingMiddleware.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,8 @@ private void MapNotAggregate(HttpContext httpContext, List<HttpContext> downstre
210210
httpContext.Items.UpsertDownstreamRequest(finished.Items.DownstreamRequest());
211211

212212
httpContext.Items.UpsertDownstreamResponse(finished.Items.DownstreamResponse());
213+
214+
httpContext.Items.SetAuthChallenge(finished.Items.AuthChallenge());
213215
}
214216

215217
private async Task<HttpContext> Fire(HttpContext httpContext, RequestDelegate next)

src/Ocelot/Responder/HttpContextResponder.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,11 @@ public async Task SetErrorResponseOnContext(HttpContext context, DownstreamRespo
8585
}
8686
}
8787

88+
public void SetAuthChallengeOnContext(HttpContext context, string challenge)
89+
{
90+
AddHeaderIfDoesntExist(context, new Header("WWW-Authenticate", new [] { challenge }));
91+
}
92+
8893
private void SetStatusCode(HttpContext context, int statusCode)
8994
{
9095
if (!context.Response.HasStarted)
Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
1-
namespace Ocelot.Responder
1+
namespace Ocelot.Responder
22
{
33
using Microsoft.AspNetCore.Http;
44
using Ocelot.Middleware;
5-
using System.Threading.Tasks;
5+
using System.Threading.Tasks;
6+
7+
public interface IHttpResponder
8+
{
9+
Task SetResponseOnHttpContext(HttpContext context, DownstreamResponse response);
610

7-
public interface IHttpResponder
8-
{
9-
Task SetResponseOnHttpContext(HttpContext context, DownstreamResponse response);
10-
1111
void SetErrorResponseOnContext(HttpContext context, int statusCode);
1212

13-
Task SetErrorResponseOnContext(HttpContext context, DownstreamResponse response);
14-
}
13+
Task SetErrorResponseOnContext(HttpContext context, DownstreamResponse response);
14+
15+
void SetAuthChallengeOnContext(HttpContext context, string challenge);
16+
}
1517
}

src/Ocelot/Responder/Middleware/ResponderMiddleware.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,15 @@ private void SetErrorResponse(HttpContext context, List<Error> errors)
6464
var downstreamResponse = context.Items.DownstreamResponse();
6565
_responder.SetErrorResponseOnContext(context, downstreamResponse);
6666
}
67+
68+
if (errors.Any(e => e.Code == OcelotErrorCode.UnauthenticatedError))
69+
{
70+
var challenge = context.Items.AuthChallenge();
71+
if (!string.IsNullOrEmpty(challenge))
72+
{
73+
_responder.SetAuthChallengeOnContext(context, challenge);
74+
}
75+
}
6776
}
6877
}
6978
}

test/Ocelot.AcceptanceTests/AuthenticationTests.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,46 @@ public void should_return_201_using_identity_server_reference_token()
256256
.BDDfy();
257257
}
258258

259+
[Fact]
260+
public void should_return_www_authenticate_header_on_401()
261+
{
262+
int port = RandomPortFinder.GetRandomPort();
263+
264+
var configuration = new FileConfiguration
265+
{
266+
Routes = new List<FileRoute>
267+
{
268+
new FileRoute
269+
{
270+
DownstreamPathTemplate = _downstreamServicePath,
271+
DownstreamHostAndPorts = new List<FileHostAndPort>
272+
{
273+
new FileHostAndPort
274+
{
275+
Host =_downstreamServiceHost,
276+
Port = port,
277+
},
278+
},
279+
DownstreamScheme = _downstreamServiceScheme,
280+
UpstreamPathTemplate = "/",
281+
UpstreamHttpMethod = new List<string> { "Get" },
282+
AuthenticationOptions = new FileAuthenticationOptions
283+
{
284+
AuthenticationProviderKey = "Test",
285+
},
286+
},
287+
},
288+
};
289+
290+
this.Given(x => _steps.GivenThereIsAConfiguration(configuration))
291+
.And(x => _steps.GivenOcelotIsRunningWithJwtAuth("Test"))
292+
.And(x => _steps.GivenIHaveNoTokenForMyRequest())
293+
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
294+
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.Unauthorized))
295+
.And(x => _steps.ThenTheResponseShouldContainAuthChallenge())
296+
.BDDfy();
297+
}
298+
259299
private void GivenThereIsAServiceRunningOn(string url, int statusCode, string responseBody)
260300
{
261301
_serviceHandler.GivenThereIsAServiceRunningOn(url, async context =>

test/Ocelot.AcceptanceTests/Steps.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -798,11 +798,57 @@ public void GivenOcelotIsRunning(OcelotPipelineConfiguration ocelotPipelineConfi
798798

799799
_ocelotClient = _ocelotServer.CreateClient();
800800
}
801+
802+
/// <summary>
803+
/// This is annoying cos it should be in the constructor but we need to set up the file before calling startup so its a step.
804+
/// </summary>
805+
public void GivenOcelotIsRunningWithJwtAuth(string authenticationProviderKey)
806+
{
807+
var builder = new ConfigurationBuilder()
808+
.SetBasePath(Directory.GetCurrentDirectory())
809+
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false)
810+
.AddJsonFile("ocelot.json", false, false)
811+
.AddEnvironmentVariables();
812+
813+
var configuration = builder.Build();
814+
_webHostBuilder = new WebHostBuilder();
815+
_webHostBuilder.ConfigureServices(s =>
816+
{
817+
s.AddSingleton(_webHostBuilder);
818+
});
819+
820+
_ocelotServer = new TestServer(_webHostBuilder
821+
.UseConfiguration(configuration)
822+
.ConfigureServices(s =>
823+
{
824+
s.AddAuthentication().AddJwtBearer(authenticationProviderKey, options =>
825+
{
826+
827+
});
828+
s.AddOcelot(configuration);
829+
})
830+
.ConfigureLogging(l =>
831+
{
832+
l.AddConsole();
833+
l.AddDebug();
834+
})
835+
.Configure(a =>
836+
{
837+
a.UseOcelot().Wait();
838+
}));
839+
840+
_ocelotClient = _ocelotServer.CreateClient();
841+
}
801842

802843
public void GivenIHaveAddedATokenToMyRequest()
803844
{
804845
_ocelotClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken);
805846
}
847+
848+
public void GivenIHaveNoTokenForMyRequest()
849+
{
850+
_ocelotClient.DefaultRequestHeaders.Authorization = null;
851+
}
806852

807853
public void GivenIHaveAToken(string url)
808854
{
@@ -1083,6 +1129,12 @@ public void ThenTheStatusCodeShouldBe(HttpStatusCode expectedHttpStatusCode)
10831129
_response.StatusCode.ShouldBe(expectedHttpStatusCode);
10841130
}
10851131

1132+
public void ThenTheResponseShouldContainAuthChallenge()
1133+
{
1134+
_response.Headers.TryGetValues("WWW-Authenticate", out var headerValue).ShouldBeTrue();
1135+
headerValue.ShouldNotBeEmpty();
1136+
}
1137+
10861138
public void ThenTheStatusCodeShouldBe(int expectedHttpStatusCode)
10871139
{
10881140
var responseStatusCode = (int)_response.StatusCode;

0 commit comments

Comments
 (0)