Web API 2, Swagger & IdentityServer3 - asp.net-web-api2

I am trying to setup a Web API with Swagger and an IdentityServer and can't figure out how to make Swagger works correctly.
My React app is working with the IdentityServer and I managed to get the ui working but when I try to activate authentication, I always get a "insufficient_scope" error.
Here's my config :
Client
public static IEnumerable<Client> Get()
{
return new[]
{
new Client
{
ClientId = "ipassportimplicit",
ClientName = "iPassport (Implicit)",
Flow = Flows.Implicit,
AllowAccessToAllScopes = true,
//redirect = URI of the React application callback page
RedirectUris = new List<string>
{
Constants.iPassportReact + "callback.html"
}
},
new Client
{
ClientId = "swaggerui",
ClientName = "Swagger (Implicit)",
Flow = Flows.Implicit,
AllowAccessTokensViaBrowser = true,
PostLogoutRedirectUris = new List<string>
{
"http://localhost:53633/swagger/"
},
AllowAccessToAllScopes = true,
RedirectUris = new List<string>
{
"http://localhost:53633/swagger/ui/o2c-html"
}
}
};
}
Scope
public static IEnumerable<Scope> Get()
{
return new List<Scope>
{
new Scope
{
Name = "passportmanagement",
DisplayName = "Passport Management",
Description = "Allow the application to manage passports on your behalf.",
Type = ScopeType.Resource
},
new Scope
{
Name = "swagger",
DisplayName = "Swagger UI",
Description = "Display Swagger UI",
Type = ScopeType.Resource
}
};
}
SwaggerConfig
public static void Register(HttpConfiguration config)
{
var thisAssembly = typeof(SwaggerConfig).Assembly;
config
.EnableSwagger(c =>
{
c.SingleApiVersion("v2", "api_iPassport");
c.OAuth2("oauth2")
.Description("OAuth2 Implicit Grant")
.Flow("implicit")
.AuthorizationUrl(Constants.iPassportSTSAuthorizationEndpoint)
.TokenUrl(Constants.iPassportSTSTokenEndpoint)
.Scopes(scopes =>
{
scopes.Add("swagger", "Swagger UI");
});
c.OperationFilter<AssignOAuth2SecurityRequirements>();
})
.EnableSwaggerUi(c =>
{
c.EnableOAuth2Support("swaggerui", "swaggerrealm", "Swagger UI");
});
}
Operation Filter
public class AssignOAuth2SecurityRequirements : IOperationFilter
{
public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
{
var actFilters = apiDescription.ActionDescriptor.GetFilterPipeline();
var allowsAnonymous = actFilters.Select(f => f.Instance).OfType<OverrideAuthorizationAttribute>().Any();
if (allowsAnonymous)
return; // must be an anonymous method
//var scopes = apiDescription.ActionDescriptor.GetFilterPipeline()
// .Select(filterInfo => filterInfo.Instance)
// .OfType<AllowAnonymousAttribute>()
// .SelectMany(attr => attr.Roles.Split(','))
// .Distinct();
if (operation.security == null)
operation.security = new List<IDictionary<string, IEnumerable<string>>>();
var oAuthRequirements = new Dictionary<string, IEnumerable<string>>
{
{"oauth2", new List<string> {"swagger"}}
};
operation.security.Add(oAuthRequirements);
}
}
Response Headers
{
"date": "Fri, 12 May 2017 03:37:08 GMT",
"www-authenticate": "Bearer error=\"insufficient_scope\"",
"x-sourcefiles": "=?UTF-8?B?TzpcTG9jYWwgV29ya3NwYWNlXFZTVFMgSUJNXFJlcG9zXFdlYkFQSVxhcGlfaVBhc3Nwb3J0XGFwaV9pUGFzc3BvcnRcYXBpXFVzZXJcR2V0?=",
"server": "Microsoft-IIS/10.0",
"x-powered-by": "ASP.NET",
"content-length": "0",
"content-type": null
}
Anything I can't see? All help appreciated!
Thanks

My problem was in my Startup.cs class of the Web API in which I didn't add the required scope to the
public void ConfigureAuth(IAppBuilder app)
{
var options = new IdentityServerBearerTokenAuthenticationOptions()
{
Authority = Constants.iPassportSTS,
RequiredScopes = new[] { "passportmanagement", "swagger" }
};
app.UseIdentityServerBearerTokenAuthentication(options);
}

Related

RavenDB The database **** is currently locked because Checking if we need to recreate indexes

I am using RavenTestDriver for my unit tests .
Here is my configuration of my test :
ConfigureServer(new TestServerOptions
{
CommandLineArgs = new System.Collections.Generic.List<string> { "--RunInMemory=true", },
FrameworkVersion = null,
}) ;
IDocumentStore store = GetDocumentStore();
try
{
store.Maintenance.ForDatabase(ravenTestDatabaseName).Send(new GetStatisticsOperation());
}
catch (DatabaseDoesNotExistException)
{
store.Maintenance.Server.Send(new CreateDatabaseOperation(new DatabaseRecord(ravenTestDatabaseName)));
}
session = store.OpenAsyncSession(new SessionOptions()
{
Database=ravenTestDatabaseName,
});
var hostBuilder = easy.api.Program.CreateHostBuilder(new string[0])
.ConfigureWebHost(webHostBuilder =>
{
webHostBuilder.UseTestServer();
})
.ConfigureServices(services =>
{
services.AddScoped<ICurrentUserService, InitRequest>();
services.AddScoped<ICacheStorage>(provider=>
{
return new Mock<ICacheStorage>().Object;
});
services.AddRavenDbAsyncSession(GetDocumentStore(new GetDocumentStoreOptions()));
services.AddScoped<IAsyncDocumentSession>((c) =>
{
return store.OpenAsyncSession(new SessionOptions()
{
Database=ravenTestDatabaseName,
});
});
});
So I have several test in my solution:
When I run each test individual the test passed .But When I run all tests together I get this error :
Raven.Client.Exceptions.Database.DatabaseDisabledException : Raven.Client.Exceptions.Database.DatabaseDisabledException: The database Test-Server is currently locked because Checking if we need to recreate indexes
at Raven.Server.Documents.DatabasesLandlord.UnloadAndLockDatabase(String dbName, String reason) in C:\Builds\RavenDB-Stable-5.2\52000\src\Raven.Server\Documents\DatabasesLandlord.cs:line 907
at Raven.Server.Web.System.AdminDatabasesHandler.Put() in C:\Builds\RavenDB-Stable-5.2\52000\src\Raven.Server\Web\System\AdminDatabasesHandler.cs:line 327
at Raven.Server.Routing.RequestRouter.HandlePath(RequestHandlerContext reqCtx) in C:\Builds\RavenDB-Stable-5.2\52000\src\Raven.Server\Routing\RequestRouter.cs:line 349
at Raven.Server.RavenServerStartup.RequestHandler(HttpContext context) in C:\Builds\RavenDB-Stable-5.2\52000\src\Raven.Server\RavenServerStartup.cs:line 174
The server at /admin/databases?name=Test-Server&replicationFactor=1&raft-request-id=d9a39f56-a1ad-44b7-b6e5-b195c1143c4b responded with status code: ServiceUnavailable.
I just leave the database name empty .
public class TestHostBuilder : RavenTestDriver, IAsyncLifetime
{
public HttpClient httpClient = null;
public IDocumentStore documentStore = null;
public async Task InitializeAsync()
{
documentStore = GetDocumentStore();
var hostBuilder = easy.api.Program.CreateHostBuilder(new string[0])
.ConfigureWebHost(webHostBuilder =>
{
webHostBuilder.UseTestServer();
})
.ConfigureServices(services =>
{
services.AddScoped<ICurrentUserService, InitRequest>();
services.AddScoped<ICacheStorage>(provider =>
{
return new Mock<ICacheStorage>().Object;
});
services.AddRavenDbAsyncSession(GetDocumentStore(new GetDocumentStoreOptions()));
services.AddTransient<IAsyncDocumentSession>((c) =>
{
return documentStore.OpenAsyncSession();
});
});
var host = hostBuilder.Start();
httpClient = host.GetTestClient();
}
public Task DisposeAsync()=> Task.CompletedTask;
}

NSwag and multiple api versions

Consider controllers below:
namespace Web.Controllers
{
[ApiVersioning("1.0")
[Route("api/v{version:apiVersion}/[controller]")]
public class Product : ApiController
{
[HttpGet("id")]
public IHttpActionResult<bool> GetProduct(Guid id)
{ /* rest of the code */ }
}
}
namespace Web.Controllers
{
[ApiVersioning("2.0")
[Route("api/v{version:apiVersion}/[controller]")]
public class Product2 : ApiController
{
[HttpGet("id")]
public IHttpActionResult<bool> GetProduct(Guid id)
{ /* rest of the code */ }
}
}
And Swagger documents in Startup class:
services.AddSwaggerDocument(config =>
{
config.DocumentName = "v1.0";
config.PostProcess = document =>
{
document.Info.Version = "v1.0";
};
});
services.AddSwaggerDocument(config =>
{
config.DocumentName = "v2.0";
config.PostProcess = document =>
{
document.Info.Version = "v2.0";
};
});
Now after testing the API browser with NSwag it ignores the versions and shows all the APIs in both v1 and v2 document.
How to tell NSwag to separate them?
I think you are missing ApiGroupNames property which is used to select Api version. Please add ApiGroupNames property like below code and let us know.
services.AddSwaggerDocument(config =>
{
config.DocumentName = "v1.0";
config.PostProcess = document =>
{
document.Info.Version = "v1.0";
};
config.ApiGroupNames = new[] { "1.0" };
});
services.AddSwaggerDocument(config =>
{
config.DocumentName = "v2.0";
config.PostProcess = document =>
{
document.Info.Version = "v2.0";
};
config.ApiGroupNames = new[] { "2.0" };
});

XMLHttpRequest Post in Razor Pages

can some one look at the code and tell me, what Am i doing wrong here? At send, I am getting 400 error.Method downlaodPDF is not even hit.Here is my code.
$('#downloadPDF').on('click', function () {
var token = $("input[name='__RequestVerificationToken']").val();
var vehicle = { "fname": "henry", "lname": "ford" };
var dataJson = JSON.stringify(vehicle);
var req = new XMLHttpRequest();
req.open("POST", "/Test?handler=DownloadPDF");
req.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
req.send(dataJson);
req.responseType = "blob";
req.onload = function (event) {
var blob = req.response;
console.log(blob.size);
var link=document.createElement('a');
link.href=window.URL.createObjectURL(blob);
link.download="Report_" + new Date() + ".pdf";
link.click(); };
});
public void OnPostDownloadPDF([FromBody]ReportInput input)
{
//My code here
HttpContext.JsReportFeature().Recipe(Recipe.ChromePdf);
}

OpenID Connect and IdentityServer4: APIs vs scopes

http://docs.identityserver.io/en/latest/reference/api_resource.html says "An API must have at least one scope."
It seems this restriction is not enforced by IdentityServer4: I have
public static IEnumerable<ApiResource> GetApis(IConfiguration config)
{
return new ApiResource[]
{
new ApiResource("orm", "Our ORM"),
};
}
(without scopes here) and my software does work and seems not to produce any errors. Do I understand correctly that here I have an error but IdentityServer4 just does not diagnose the error?
Moreover, I have
new Client
{
ClientId = "mvc",
ClientName = "MVC Client",
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
ClientSecrets = { new Secret("XXX".Sha256()) },
RedirectUris = { ormClientURL + "/signin-oidc" },
FrontChannelLogoutUri = ormClientURL + "/signout-oidc",
PostLogoutRedirectUris = { ormClientURL + "/signout-callback-oidc" },
AllowOfflineAccess = true,
AllowedScopes = { "openid", "profile", "orm" }
},
and it works despite I have no scope "orm" defined.
Why does it indeed work?
How to fix this properly? (Which clients and scopes should I define?)
It appears that the ApiResource constructor adds a Scope using the name and displayName provided:
public ApiResource(string name, string displayName)
: this(name, displayName, null)
{
}
public ApiResource(string name, string displayName, IEnumerable<string> claimTypes)
{
if (name.IsMissing()) throw new ArgumentNullException(nameof(name));
Name = name;
DisplayName = displayName;
Scopes.Add(new Scope(name, displayName));
if (!claimTypes.IsNullOrEmpty())
{
foreach (var type in claimTypes)
{
UserClaims.Add(type);
}
}
}
https://github.com/IdentityServer/IdentityServer4/blob/master/src/Storage/src/Models/ApiResource.cs

How to create user with ASP.NET Core 2.2

The following controller should create a user within the local SQL Server database, but when refreshing the dbo.AspNetUsers table, there are no users found. What am I missing here?
UserController.cs
using System.Threading.Tasks;
using backend.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
namespace backend.Controllers
{
[Route("api/[controller]/[action]")]
[ApiController]
public class UserController : Controller
{
private readonly UserManager<AppUser> userManager;
public UserController(UserManager<AppUser> usrMgr)
{
userManager = usrMgr;
}
[HttpPost]
public async Task<IActionResult> Register([FromBody]CreateUserModel model)
{
AppUser user = new AppUser
{
UserName = model.Email,
Email = model.Email
};
var result = await userManager.CreateAsync(user, model.Password);
return CreatedAtAction(nameof(Register), user);
}
}
}
Postman returns the following 201 Created body:
{
"id": "random string of characters here",
"userName": "user#example.com",
"normalizedUserName": null,
"email": "user#example.com",
"normalizedEmail": null,
"emailConfirmed": false,
"passwordHash": null,
"securityStamp": null,
"concurrencyStamp": ""random string of characters here",
"phoneNumber": null,
"phoneNumberConfirmed": false,
"twoFactorEnabled": false,
"lockoutEnd": null,
"lockoutEnabled": false,
"accessFailedCount": 0
}
Solution
After using the answer by Volodymyr Bilyachat, the failed result object passed to Postman revealed that the test password that I was using was too weak. I then knew to add the following password settings to Startup.cs, and to choose a password that would work.
services.Configure<IdentityOptions>(options =>
{
// Password settings.
options.Password.RequireDigit = true;
options.Password.RequireLowercase = true;
options.Password.RequireNonAlphanumeric = true;
options.Password.RequireUppercase = true;
options.Password.RequiredLength = 6;
options.Password.RequiredUniqueChars = 1;
// Lockout settings.
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.AllowedForNewUsers = true;
// User settings.
options.User.AllowedUserNameCharacters =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._#+";
options.User.RequireUniqueEmail = false;
});
You need to check result
var result = await userManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
return CreatedAtAction(nameof(Register), user);
}
// handle bad request
so it can be that result contains error code.