diff --git a/Readme.md b/Readme.md
index 7483477..8458306 100644
--- a/Readme.md
+++ b/Readme.md
@@ -1,6 +1,6 @@
# Introduction
-![Under Development](https://img.shields.io/badge/status-Under%20Development-orange) ![Build Status](https://circleci.com/gh/dnutiu/retroactiune.svg?style=svg)
+![Build Status](https://circleci.com/gh/dnutiu/retroactiune.svg?style=svg)
![GitHub commit activity](https://img.shields.io/github/commit-activity/m/dnutiu/retroactiune) ![GitHub repo size](https://img.shields.io/github/repo-size/dnutiu/retroactiune) ![GitHub top language](https://img.shields.io/github/languages/top/dnutiu/retroactiune)
![Swagger API](./docs/retroactiune_swagger.png)
@@ -31,6 +31,17 @@ The application code is organized using the [Clean Architecture](https://docs.mi
![Example deployment architecture](./docs/app_architecture_layers.png)
+## Authorization Provider
+
+An external Authorization provider is required in order to run the API, the provider needs to support
+Bearer tokens and HS256 key signing algorithm. _RS256 is currently not supported._
+
+I recommend that you start with [Auth0](https://auth0.com/) and their free tier.
+
+See the following resources:
+- https://auth0.com/docs/get-started/set-up-apis
+- https://developers.redhat.com/blog/2020/01/29/api-login-and-jwt-token-generation-using-keycloak#set_up_a_client
+
## Developing
To install the dependencies run `dotnet restore`.
@@ -48,4 +59,6 @@ _Note: [Docker](https://www.docker.com/) and [Docker-Compose](https://docs.docke
```bash
docker-compose up -d
dotnet test
-```
\ No newline at end of file
+```
+
+The projects has ~96% code coverage.
\ No newline at end of file
diff --git a/Retroactiune.IntegrationTests/Retroactiune.IntegrationTests.csproj b/Retroactiune.IntegrationTests/Retroactiune.IntegrationTests.csproj
index a52df2b..f775be0 100644
--- a/Retroactiune.IntegrationTests/Retroactiune.IntegrationTests.csproj
+++ b/Retroactiune.IntegrationTests/Retroactiune.IntegrationTests.csproj
@@ -11,7 +11,7 @@
-
+
diff --git a/Retroactiune.WebAPI/Controllers/FeedbackReceivers.FeedbacksController.cs b/Retroactiune.WebAPI/Controllers/FeedbackReceivers.FeedbacksController.cs
index b624b46..cad6cad 100644
--- a/Retroactiune.WebAPI/Controllers/FeedbackReceivers.FeedbacksController.cs
+++ b/Retroactiune.WebAPI/Controllers/FeedbackReceivers.FeedbacksController.cs
@@ -1,5 +1,6 @@
using System.Linq;
using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Retroactiune.Core.Entities;
@@ -77,9 +78,11 @@ namespace Retroactiune.Controllers
/// The feedback has been added.
/// The request is invalid.
///
+ [Authorize]
[HttpGet("{guid}/feedbacks")]
[ProducesResponseType(typeof(NoContentResult), StatusCodes.Status204NoContent)]
[ProducesResponseType(typeof(BasicResponse), StatusCodes.Status400BadRequest)]
+ [ProducesResponseType( StatusCodes.Status401Unauthorized)]
public async Task GetFeedbacks(string guid, [FromQuery] ListFeedbacksFiltersDto filters)
{
var feedbacksListFilters = _mapper.Map(filters);
diff --git a/Retroactiune.WebAPI/Controllers/FeedbackReceiversController.cs b/Retroactiune.WebAPI/Controllers/FeedbackReceiversController.cs
index 5576e55..8473a8c 100644
--- a/Retroactiune.WebAPI/Controllers/FeedbackReceiversController.cs
+++ b/Retroactiune.WebAPI/Controllers/FeedbackReceiversController.cs
@@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using AutoMapper;
+using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
@@ -46,10 +47,12 @@ namespace Retroactiune.Controllers
/// The list of FeedbackReceivers
/// A BasicResponse indicating success.
/// Returns an ok message.
- /// If the items is invalid
+ /// If the items is invalid
+ [Authorize]
[HttpPost]
[ProducesResponseType(typeof(BasicResponse), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType( StatusCodes.Status401Unauthorized)]
public async Task Post([Required] IEnumerable items)
{
var feedbackReceiversDto = items.ToList();
@@ -77,9 +80,11 @@ namespace Retroactiune.Controllers
/// A NoContent result.
/// The delete is submitted.
/// The request is invalid.
+ [Authorize]
[HttpDelete("{guid}")]
[ProducesResponseType(typeof(NoContentResult), StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType( StatusCodes.Status401Unauthorized)]
public async Task Delete(
[StringLength(24, ErrorMessage = "invalid guid, must be 24 characters", MinimumLength = 24)]
string guid)
@@ -96,11 +101,13 @@ namespace Retroactiune.Controllers
/// A Ok result with a .
/// The item returned successfully.
/// The request is invalid.
- /// The item was not found.
+ /// The item was not found.
+ [Authorize]
[HttpGet("{guid}")]
[ProducesResponseType(typeof(FeedbackReceiverOutDto), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(BasicResponse), StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType( StatusCodes.Status401Unauthorized)]
public async Task Get(
[StringLength(24, ErrorMessage = "invalid guid, must be 24 characters", MinimumLength = 24)]
string guid)
@@ -126,10 +133,12 @@ namespace Retroactiune.Controllers
/// If set, it will limit the results to N items. Allowed range is 1-1000.
/// A Ok result with a list of .
/// The a list is returned.
- /// The request is invalid.
+ /// The request is invalid.
[HttpGet]
+ [Authorize]
[ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(BasicResponse), StatusCodes.Status400BadRequest)]
+ [ProducesResponseType( StatusCodes.Status401Unauthorized)]
public async Task List([FromQuery] IEnumerable filter,
[RangeAttribute(1, int.MaxValue, ErrorMessage = "offset is out of range, allowed ranges [1-IntMax]"),
FromQuery]
@@ -148,8 +157,10 @@ namespace Retroactiune.Controllers
/// The request is invalid.
///
[HttpDelete]
+ [Authorize]
[ProducesResponseType(typeof(NoContentResult), StatusCodes.Status204NoContent)]
[ProducesResponseType(typeof(BasicResponse), StatusCodes.Status400BadRequest)]
+ [ProducesResponseType( StatusCodes.Status401Unauthorized)]
public async Task DeleteMany([Required] IEnumerable ids)
{
try
diff --git a/Retroactiune.WebAPI/Controllers/TokensController.cs b/Retroactiune.WebAPI/Controllers/TokensController.cs
index c128295..5a3cfc3 100644
--- a/Retroactiune.WebAPI/Controllers/TokensController.cs
+++ b/Retroactiune.WebAPI/Controllers/TokensController.cs
@@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using AutoMapper;
+using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
@@ -39,8 +40,10 @@ namespace Retroactiune.Controllers
/// A list of tokens.
/// The request is invalid.
[HttpGet]
+ [Authorize]
[ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType( StatusCodes.Status401Unauthorized)]
public async Task ListTokens([FromQuery] ListTokensFiltersDto filtersDto)
{
try
@@ -68,8 +71,10 @@ namespace Retroactiune.Controllers
/// Returns ok.
/// If the items is invalid
[HttpPost]
+ [Authorize]
[ProducesResponseType(typeof(BasicResponse), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType( StatusCodes.Status401Unauthorized)]
public async Task GenerateTokens([Required] GenerateTokensDto generateTokensDto)
{
var feedbackReceiverId = generateTokensDto.FeedbackReceiverId;
@@ -99,8 +104,10 @@ namespace Retroactiune.Controllers
/// The request is invalid.
///
[HttpDelete]
+ [Authorize]
[ProducesResponseType(typeof(NoContentResult), StatusCodes.Status204NoContent)]
[ProducesResponseType(typeof(BasicResponse), StatusCodes.Status400BadRequest)]
+ [ProducesResponseType( StatusCodes.Status401Unauthorized)]
public async Task DeleteTokens([Required] IEnumerable tokenIds)
{
try
@@ -124,10 +131,12 @@ namespace Retroactiune.Controllers
/// The guid of the item to be deleted.
/// A NoContent result.
/// The delete is submitted.
- /// The request is invalid.
+ /// The request is invalid.
+ [Authorize]
[HttpDelete("{guid}")]
[ProducesResponseType(typeof(NoContentResult), StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType( StatusCodes.Status401Unauthorized)]
public async Task DeleteToken(
[StringLength(24, ErrorMessage = "invalid guid, must be 24 characters", MinimumLength = 24)]
string guid)
diff --git a/Retroactiune.WebAPI/Retroactiune.WebAPI.csproj b/Retroactiune.WebAPI/Retroactiune.WebAPI.csproj
index 5e6d1d2..41b6737 100644
--- a/Retroactiune.WebAPI/Retroactiune.WebAPI.csproj
+++ b/Retroactiune.WebAPI/Retroactiune.WebAPI.csproj
@@ -1,26 +1,24 @@
+
-
-
- netcoreapp3.1
- Retroactiune
- true
- 1591
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ netcoreapp3.1
+ Retroactiune
+ true
+ 1591
+ 0efb2158-cb38-4e00-9900-48a0fa4ce3c1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Retroactiune.WebAPI/Startup.cs b/Retroactiune.WebAPI/Startup.cs
index 3adbfdf..ada12f8 100644
--- a/Retroactiune.WebAPI/Startup.cs
+++ b/Retroactiune.WebAPI/Startup.cs
@@ -1,6 +1,8 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
+using System.Text;
+using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
@@ -8,6 +10,8 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
+using Microsoft.IdentityModel.Tokens;
+using Microsoft.OpenApi.Models;
using MongoDB.Driver;
using Prometheus;
using Retroactiune.Core.Interfaces;
@@ -20,8 +24,6 @@ namespace Retroactiune
[ExcludeFromCodeCoverage]
public class Startup
{
- // TODO: External auth provider.
- // TODO: Improve coverage.
// TODO: UI?
public Startup(IConfiguration configuration)
{
@@ -52,12 +54,51 @@ namespace Retroactiune
return new MongoClient(settings.Value.ConnectionString);
});
+ services.AddAuthentication(options =>
+ {
+ options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
+ options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
+ }).AddJwtBearer(options =>
+ {
+ options.TokenValidationParameters = new TokenValidationParameters
+ {
+ ValidIssuer = Configuration.GetSection("AuthorizationProvider:Domain").Value,
+ ValidAudience = Configuration.GetSection("AuthorizationProvider:Audience").Value,
+ IssuerSigningKey = new SymmetricSecurityKey(
+ Encoding.UTF8.GetBytes(Configuration.GetSection("AuthorizationProvider:SymmetricSecurityKey")
+ .Value))
+ };
+ });
+
// WebAPI
services.AddControllers();
services.AddSwaggerGen(c =>
{
var filePath = Path.Combine(AppContext.BaseDirectory, "Retroactiune.WebAPI.xml");
c.IncludeXmlComments(filePath);
+ c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
+ {
+ Type = SecuritySchemeType.Http,
+ BearerFormat = "JWT",
+ In = ParameterLocation.Header,
+ Scheme = "bearer",
+ Description = "Please insert JWT token into field"
+ });
+
+ c.AddSecurityRequirement(new OpenApiSecurityRequirement
+ {
+ {
+ new OpenApiSecurityScheme
+ {
+ Reference = new OpenApiReference
+ {
+ Type = ReferenceType.SecurityScheme,
+ Id = "Bearer"
+ }
+ },
+ new string[] { }
+ }
+ });
});
}
@@ -70,7 +111,7 @@ namespace Retroactiune
}
app.UseMetricServer();
-
+
app.UseSwagger();
app.UseSwaggerUI(c =>
{
@@ -79,14 +120,14 @@ namespace Retroactiune
});
app.UseHttpsRedirection();
-
+
app.UseSentryTracing();
app.UseRouting();
+ app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
-
logger.LogInformation("Running");
}
}
diff --git a/Retroactiune.WebAPI/TestTokenAuthenticationHandler.cs b/Retroactiune.WebAPI/TestTokenAuthenticationHandler.cs
new file mode 100644
index 0000000..28a771a
--- /dev/null
+++ b/Retroactiune.WebAPI/TestTokenAuthenticationHandler.cs
@@ -0,0 +1,33 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Security.Claims;
+using System.Text.Encodings.Web;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Retroactiune
+{
+ public class TestAuthenticationOptions : AuthenticationSchemeOptions
+ {
+ }
+
+ [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")]
+ public class TestTokenAuthenticationHandler : AuthenticationHandler
+ {
+
+ public TestTokenAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger,
+ UrlEncoder encoder, ISystemClock clock)
+ : base(options, logger, encoder, clock)
+ {
+ }
+
+ protected override Task HandleAuthenticateAsync()
+ {
+ var claims = new[] {new Claim("token", "allow_all")};
+ var identity = new ClaimsIdentity(claims, nameof(TestTokenAuthenticationHandler));
+ var ticket = new AuthenticationTicket(new ClaimsPrincipal(identity), Scheme.Name);
+ return Task.FromResult(AuthenticateResult.Success(ticket));
+ }
+ }
+}
\ No newline at end of file
diff --git a/Retroactiune.WebAPI/TestingStartup.cs b/Retroactiune.WebAPI/TestingStartup.cs
index b33feaf..2108ebc 100644
--- a/Retroactiune.WebAPI/TestingStartup.cs
+++ b/Retroactiune.WebAPI/TestingStartup.cs
@@ -42,7 +42,12 @@ namespace Retroactiune
var settings = i.GetService>();
return new MongoClient(settings.Value.ConnectionString);
});
-
+
+ services.AddAuthentication(options =>
+ {
+ options.DefaultAuthenticateScheme = "Bearer";
+ }).AddScheme ("Bearer", o => { });
+
// WebAPI
services.AddControllers();
}
@@ -52,6 +57,7 @@ namespace Retroactiune
{
app.UseHttpsRedirection();
app.UseRouting();
+ app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
}
diff --git a/Retroactiune.WebAPI/appsettings.json b/Retroactiune.WebAPI/appsettings.json
index 0a87ca2..3d2d0b6 100644
--- a/Retroactiune.WebAPI/appsettings.json
+++ b/Retroactiune.WebAPI/appsettings.json
@@ -1,4 +1,9 @@
{
+ "AuthorizationProvider": {
+ "Domain": "Your AuthProvider Domain",
+ "Audience": "Your API Url",
+ "SymmetricSecurityKey": "HS256 secret"
+ },
"Sentry": {
"Dsn": "",
"MaxRequestBodySize": "Always",