Custom OpenIdClient for Customer URL in MVC 4 - asp.net-mvc-4

I'm working with the default template for MVC 4 and trying to add my own openID provider for example http://steamcommunity.com/dev to the list of openID logins and an openID box where the user can type in their openID information.
To add Google I just un-comment
OAuthWebSecurity.RegisterGoogleClient();
as for other custom solutions you can do something like
OAuthWebSecurity.RegisterClient(new SteamClient(),"Steam",null);
The trouble I have is creating SteamClient (or a generic one) http://blogs.msdn.com/b/webdev/archive/2012/08/23/plugging-custom-oauth-openid-providers.aspx doesn't show anywhere to change the URL.

I think the reason I could not find the answer is that most people thought it was common sense. I prefer my sense to be uncommon.
public class OidCustomClient : OpenIdClient
{
public OidCustomClient() : base("Oid", "http://localhost:5004/") { }
}

Based on #Jeff's answer I created a class to handle Stack Exchange OpenID.
Register:
OAuthWebSecurity.RegisterClient(new StackExchangeOpenID());
Class:
public class StackExchangeOpenID : OpenIdClient
{
public StackExchangeOpenID()
: base("stackexchange", "https://openid.stackexchange.com")
{
}
protected override Dictionary<string, string> GetExtraData(IAuthenticationResponse response)
{
FetchResponse fetchResponse = response.GetExtension<FetchResponse>();
if (fetchResponse != null)
{
var extraData = new Dictionary<string, string>();
extraData.Add("email", fetchResponse.GetAttributeValue(WellKnownAttributes.Contact.Email));
extraData.Add("name", fetchResponse.GetAttributeValue(WellKnownAttributes.Name.FullName));
return extraData;
}
return null;
}
protected override void OnBeforeSendingAuthenticationRequest(IAuthenticationRequest request)
{
var fetchRequest = new FetchRequest();
fetchRequest.Attributes.AddRequired(WellKnownAttributes.Contact.Email);
fetchRequest.Attributes.AddRequired(WellKnownAttributes.Name.FullName);
request.AddExtension(fetchRequest);
}
}
Retrieving extra data:
var result = OAuthWebSecurity.VerifyAuthentication();
result.ExtraData["email"];
result.ExtraData["name"];

Related

Custom OpenIddictApplication field always returns null

I am using OpenIDDict and extending OpenIddictEntityFrameworkCoreApplication to include my own custom field:
public class TenantApplication : OpenIddictEntityFrameworkCoreApplication<long, TenantAuthorization, TenantToken> {
public long? TenantID { get; set; }
}
public class TenantAuthorization : OpenIddictEntityFrameworkCoreAuthorization<long, TenantApplication, TenantToken> { }
public class TenantScope : OpenIddictEntityFrameworkCoreScope<long> { }
public class TenantToken : OpenIddictEntityFrameworkCoreToken<long, TenantApplication, TenantAuthorization> { }
I register under AddDbContext:
builder.Services.AddDbContext<ApplicationDbContext>(options => {
options.UseNpgsql(connectionString);
options.UseOpenIddict<TenantApplication, TenantAuthorization, TenantScope, TenantToken, long>();
});
and also under AddCore:
// Register the OpenIddict core components.
.AddCore(options =>
options.UseEntityFrameworkCore()
.UseDbContext<ApplicationDbContext>()
.ReplaceDefaultEntities<TenantApplication, TenantAuthorization, TenantScope, TenantToken, long>();
options.UseQuartz();
})
In my Worker, I create the application record if it doesn't exist, but TenantID is always inserted with null:
if (await manager.FindByClientIdAsync("postman", cancellationToken) is null)
{
await manager.CreateAsync(new TokenOpenIddictApplicationDescriptor
{
TenantID = 2,
ClientId = "postman",
ClientSecret = "388D45FA-B36B-4988-BA59-B187D329C207",
DisplayName = "My client application",
Permissions =
{
Permissions.Endpoints.Token,
Permissions.GrantTypes.ClientCredentials
}
});
Similarly, manager.FindByClientIdAsync also returns the Application instance with a null TenantID (after I manually set it in the db). What step am I missing?
I figured this out on my own by diving into the OpenIDDict source code. In order to extend the Application fields, I extended OpenIddictApplicationManager and OpenIddictEntityFrameworkCoreApplicationStore. OpenIddictApplicationManager has a PopulateAsync method which converts a OpenIddictApplicationDescriptor to an Application instance. You need to override this method so that your custom Application fields will be set.
When initializing everything in Program.cs you also need to add your custom store and replace the application manager.
options.AddApplicationStore<TenantOpenIddictEntityFrameworkCoreApplicationStore>();
options.ReplaceApplicationManager<TenantOpenIddictApplicationManager>();
Maybe there is a better way of doing this, but this worked for me. It would be nice if there was some more easier to find examples with more complex scenarios.

How to define links for OAS3 using Swashbuckle?

I have noticed that in the Swagger UI v3 and in OAS3 we now have support for something called "links"
But I cant really figure out if its possible to use this feature with Swashbuckle, and if it is.. then how? Been searching the net and haven't found anything regarding this..
Anyone been able to use links with Swashbuckle?
You can use an OperationFilter. Create a class that implements IOperationFilter
public class MyLinkFilter : IOperationFilter
{
into it select the response for which you want to add Links.
public void Apply(OpenApiOperation operation, OperationFilterContext context)
var responses = operation.Responses;
var response = responses.FirstOrDefault(r => r.Key == "200").Value;
then update the Links property
response.Links = new Dictionary<string, OpenApiLink>
{
{
"YourKey"
,new OpenApiLink {
OperationId = "YourOperationId",
Description = ".............",
Parameters = new Dictionary<string, RuntimeExpressionAnyWrapper>
{
{
"yourParam", new RuntimeExpressionAnyWrapper
{
Any = new OpenApiString("$request.path.number")
}
}
}
}
}
};
Register your OperationFilter into startup.cs
services.AddSwaggerGen(options =>
{
options.OperationFilter<MyLinkFilter>();
});
OpenAPI, Response
OpenAPI, Link
Finally, you'll have to implement a mechanism to apply the links to the good Action in your controller.

RequireAuthorization and Swashbuckle IOperationFilter

I am looking for a way to determine if endpoint requires authorization (.Net Core 3.1) using IOperationFilter.
If Authorization is setup via filter or explicitly as attribute, it can be found in OperationFilterContext context.ApiDescription.ActionDescriptor.FilterDescriptors.Select(filterInfo => filterInfo.Filter).Any(filter => filter is AuthorizeFilter) and context.ApiDescription.CustomAttributes().OfType<AuthorizeAttribute>().
But if authorization is set as
endpoints.MapControllers().RequireAuthorization();, which should add AuthorizationAttribute to all endpoints, it is not appeared neither in filters nor in attributes. Any thoughts on how to catch if auth is applied to endpoints in this case?
I was able to beat this today like so (swashbuckle 5.63):
Make a new class like this
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Authorization;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace YourNameSpace
{
public class SwaggerGlobalAuthFilter : IOperationFilter
{
public void Apply( OpenApiOperation operation, OperationFilterContext context )
{
context.ApiDescription.TryGetMethodInfo( out MethodInfo methodInfo );
if ( methodInfo == null )
{
return;
}
var hasAllowAnonymousAttribute = false;
if ( methodInfo.MemberType == MemberTypes.Method )
{
// NOTE: Check the controller or the method itself has AllowAnonymousAttribute attribute
hasAllowAnonymousAttribute =
methodInfo.DeclaringType.GetCustomAttributes( true ).OfType<AllowAnonymousAttribute>().Any() ||
methodInfo.GetCustomAttributes( true ).OfType<AllowAnonymousAttribute>().Any();
}
if ( hasAllowAnonymousAttribute )
{
return;
}
// NOTE: This adds the "Padlock" icon to the endpoint in swagger,
// we can also pass through the names of the policies in the List<string>()
// which will indicate which permission you require.
operation.Security = new List<OpenApiSecurityRequirement>
{
new OpenApiSecurityRequirement()
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "oauth2" // note this 'Id' matches the name 'oauth2' defined in the swagger extensions config section below
},
Scheme = "oauth2",
Name = "Bearer",
In = ParameterLocation.Header,
},
new List<string>()
}
}
};
}
}
}
In swagger config extensions
options.AddSecurityDefinition( "oauth2", new OpenApiSecurityScheme
{
Type = SecuritySchemeType.OAuth2,
Flows = new OpenApiOAuthFlows
{
Implicit = new OpenApiOAuthFlow
{
//_swaggerSettings is a custom settings object of our own
AuthorizationUrl = new Uri( _swaggerSettings.AuthorizationUrl ),
Scopes = _swaggerSettings.Scopes
}
}
} );
options.OperationFilter<SwaggerGlobalAuthFilter>();
Put together from docs, other SO and decompiled code of built-in SecurityRequirementsOperationFilter
AFAIK, it is defining a global auth setup for all your routed endpoints except those that explicitly have AllowAnonymousAttribute on controller or endpoint. since, as your original question hints at, using the extension RequireAuthorization() when setting up routing implicitly puts that attribute on all endpoints and the built-in SecurityRequirementsOperationFilter which detect the Authorize attribute fails to pick it up. Since your routing setup effectively is putting Authorize on every controller/route it seems setting up a default global filter like this that excludes AllowAnonymous would be in line with what you are configuring in the pipeline.
I suspect there may be a more 'built-in' way of doing this, but I could not find it.
Apparently, this is an open issue on the NSwag repo as well (for people like me that drive by with the same issue, but with NSwag instead of Swashbuckle):
https://github.com/RicoSuter/NSwag/issues/2817
Where there's also another example of solving the issue (not only securityrequirement, but also its scopes).
I know it's been a long time since this question was asked.
But I was facing a similar issue, and following the advice from an issue in GitHub here, managed to resolve it using this implementation of IOperationFilter (and now works like a charm):
public class AuthorizeCheckOperationFilter : IOperationFilter
{
private readonly EndpointDataSource _endpointDataSource;
public AuthorizeCheckOperationFilter(EndpointDataSource endpointDataSource)
{
_endpointDataSource = endpointDataSource;
}
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var descriptor = _endpointDataSource.Endpoints.FirstOrDefault(x =>
x.Metadata.GetMetadata<ControllerActionDescriptor>() == context.ApiDescription.ActionDescriptor);
var hasAuthorize = descriptor.Metadata.GetMetadata<AuthorizeAttribute>()!=null;
var allowAnon = descriptor.Metadata.GetMetadata<AllowAnonymousAttribute>() != null;
if (!hasAuthorize || allowAnon) return;
operation.Responses.Add("401", new OpenApiResponse { Description = "Unauthorized" });
operation.Responses.Add("403", new OpenApiResponse { Description = "Forbidden" });
operation.Security = new List<OpenApiSecurityRequirement>
{
new()
{
[
new OpenApiSecurityScheme {Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "oauth2"}
}
] = new[] {"api1"}
}
};
}
}
The issue stated this:
ControllerActionDescriptor.EndpointMetadata only reflects the metadata
discovered on the controller action. Any metadata configured via the
endpoint APIs do not show up here. It was primarily the reason we
documented it as being infrastructure-only since it's a bit confusing
to use.
There's a couple of options you could use
a) You could decorate your controllers using [Authorize]. That should allow the metadata to show up in the property.
b) You could look up the metadata by reading from EndpointDataSource.

Naming conventions for view pages and setting controller action for view

I am unsure on how I should be naming my View pages, they are all CamelCase.cshtml, that when viewed in the browser look like "http://www.website.com/Home/CamelCase".
When I am building outside of .NET my pages are named like "this-is-not-camel-case.html". How would I go about doing this in my MVC4 project?
If I did go with this then how would I tell the view to look at the relevant controller?
Views/Home/camel-case.cshtml
Fake edit: Sorry if this has been asked before, I can't find anything via search or Google. Thanks.
There are a few ways you can do this:
Name all of your views in the style you would like them to show up in the url
This is pretty simple, you just add the ActionName attribute to all of your actions and specify them in the style you would like your url to look like, then rename your CamelCase.cshtml files to camel-case.cshtml files.
Use attribute routing
Along the same lines as above, there is a plugin on nuget to enable attribute routing which lets you specify the full url for each action as an attribute on the action. It has convention attributes to help you out with controller names and such as well. I generally prefer this approach because I like to be very explicit with the routes in my application.
A more framework-y approach
It's probably possible to do something convention based by extending the MVC framework, but it would be a decent amount of work. In order to select the correct action on a controller, you'd need to map the action name on its way in to MVC to its CamelCase equivalent before the framework uses it to locate the action on the controller. The easiest place to do this is in the Route, which is the last thing to happen before the MVC framework takes over the request. You'll also need to convert the other way on the way out so the urls generated look like you want them to.
Since you don't really want to alter the existing method to register routes, it's probably best write a function in application init that loops over all routes after they have been registered and wraps them with your new functionality.
Here is an example route and modifications to application start that achieve what you are trying to do. I'd still go with the route attribute approach however.
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
WebApiConfig.Register(GlobalConfiguration.Configuration);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
WrapRoutesWithNamingConvention(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
AuthConfig.RegisterAuth();
}
private void WrapRoutesWithNamingConvention(RouteCollection routes)
{
var wrappedRoutes = routes.Select(m => new ConventionRoute(m)).ToList();
routes.Clear();
wrappedRoutes.ForEach(routes.Add);
}
private class ConventionRoute : Route
{
private readonly RouteBase baseRoute;
public ConventionRoute(RouteBase baseRoute)
: base(null, null)
{
this.baseRoute = baseRoute;
}
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var baseRouteData = baseRoute.GetRouteData(httpContext);
if (baseRouteData == null) return null;
var actionName = baseRouteData.Values["action"] as string;
var convertedActionName = ConvertHyphensToPascalCase(actionName);
baseRouteData.Values["action"] = convertedActionName;
return baseRouteData;
}
private string ConvertHyphensToPascalCase(string hyphens)
{
var capitalParts = hyphens.Split('-').Select(m => m.Substring(0, 1).ToUpper() + m.Substring(1));
var pascalCase = String.Join("", capitalParts);
return pascalCase;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
var valuesClone = new RouteValueDictionary(values);
var pascalAction = valuesClone["action"] as string;
var hyphens = ConvertPascalCaseToHyphens(pascalAction);
valuesClone["action"] = hyphens;
var baseRouteVirtualPath = baseRoute.GetVirtualPath(requestContext, valuesClone);
return baseRouteVirtualPath;
}
private string ConvertPascalCaseToHyphens(string pascal)
{
var pascalParts = new List<string>();
var currentPart = new StringBuilder();
foreach (var character in pascal)
{
if (char.IsUpper(character) && currentPart.Length > 0)
{
pascalParts.Add(currentPart.ToString());
currentPart.Clear();
}
currentPart.Append(character);
}
if (currentPart.Length > 0)
{
pascalParts.Add(currentPart.ToString());
}
var lowers = pascalParts.Select(m => m.ToLower());
var hyphens = String.Join("-", lowers);
return hyphens;
}
}
}

FluentSecurity 2.0 support for action with parameters

In my .net mvc 4 app I am using the latest release of FluentSecurity (1.4) in order to secure my actions.
Here is an example that illustrates my problem:
Suppose I have a controller with 2 edit actions (get and post):
public class MyController : Controller
{
//
// GET: /My/
public ActionResult Edit(decimal id)
{
var modelToReturn = GetFromDb(id);
return View(modelToReturn);
}
[HttpPost]
public ActionResult Edit(MyModel model)
{
Service.saveToDb(model);
return View(model);
}
}
Now, I would like to have a different security policy for each action. To do that I define (using fluent security):
configuration.For<MyController>(x => x.Edit(0))
.AddPolicy(new MyPolicy("my.VIEW.permission"));
configuration.For<MyController>(x => x.Edit(null))
.AddPolicy(new MyPolicy("my.EDIT.permission"));
The first configuration refers to the get while the second to the post.
If you wonder why I'm sending dummy params you can have a look here and here.
Problem is that fluent security can't tell the difference between those 2, hence this doesn't work.
Couldn't find a way to overcome it (I'm open for ideas) and I wonder if installing the new 2.0 beta release can resolve this issue.
Any ideas?
It is currently not possible to apply different policies to each signature in FluentSecurity. This is because FluentSecurity can not know what signature will be called by ASP.NET MVC. All it knows is the name of the action. So FluentSecurity has to treat both action signatures as a single action.
However, you can apply multiple policies to the same action (you are not limited to have a single policy per action). With this, you can apply an Http verb filter for each of the policies. Below is an example of what it could look like:
1) Create a base policy you can inherit from
public abstract class HttpVerbFilteredPolicy : ISecurityPolicy
{
private readonly List<HttpVerbs> _httpVerbs;
protected HttpVerbFilteredPolicy(params HttpVerbs[] httpVerbs)
{
_httpVerbs = httpVerbs.ToList();
}
public PolicyResult Enforce(ISecurityContext securityContext)
{
HttpVerbs httpVerb;
Enum.TryParse(securityContext.Data.HttpVerb, true, out httpVerb);
return !_httpVerbs.Contains(httpVerb)
? PolicyResult.CreateSuccessResult(this)
: EnforcePolicy(securityContext);
}
protected abstract PolicyResult EnforcePolicy(ISecurityContext securityContext);
}
2) Create your custom policy
public class CustomPolicy : HttpVerbFilteredPolicy
{
private readonly string _role;
public CustomPolicy(string role, params HttpVerbs[] httpVerbs) : base(httpVerbs)
{
_role = role;
}
protected override PolicyResult EnforcePolicy(ISecurityContext securityContext)
{
var accessAllowed = //... Do your checks here;
return accessAllowed
? PolicyResult.CreateSuccessResult(this)
: PolicyResult.CreateFailureResult(this, "Access denied");
}
}
3) Add the HTTP verb of the current request to the Data property of ISecurityContext and secure your actions
SecurityConfigurator.Configure(configuration =>
{
// General setup goes here...
configuration.For<MyController>(x => x.Edit(0)).AddPolicy(new CustomPolicy("my.VIEW.permission", HttpVerbs.Get));
configuration.For<MyController>(x => x.Edit(null)).AddPolicy(new CustomPolicy("my.EDIT.permission", HttpVerbs.Post));
configuration.Advanced.ModifySecurityContext(context => context.Data.HttpVerb = HttpContext.Current.Request.HttpMethod);
});