I have my own authorization server built on top identityserver4 where I want to secure all apis on a host. System is just a simple mimic of google developers or facebook developers where application owners sign up and get client id and client secrets for access grant on apis.
So I followed client_credentials flow on identityserver4 samples. All working fine. I built a public UI for app owners to create apps and choose which apis to access from their apps. I make use of IConfigurationDbContext for CRUD procecces on internal tables of identityserver.
The problem is I couldn't find a way to secure apis based on app owners' choices, when a developer crate an app and choose a few logical endpoints to access, they still can reach all enpoints. What I have done is as follows;
Authorization Server Startup
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryCaching()
.AddOperationalStore(storeOpitons =>
{
storeOpitons.ConfigureDbContext = builder =>
builder.UseSqlServer(Configuration.GetConnectionString("Default"),
sql => sql.MigrationsAssembly(migrationsAssembly));
})
.AddConfigurationStore(storeOptions =>
{
storeOptions.ConfigureDbContext = builder =>
builder.UseSqlServer(Configuration.GetConnectionString("Default"),
sql => sql.MigrationsAssembly(migrationsAssembly));
});
Saving Client Method
public IActionResult SaveApp(ClientViewModel model, List<SelectedApi> selectedApis)
{
//ommited for brevity
Client client = new Client
{
Description = model.Description,
ClientName = model.Name,
RedirectUris = new[] { model.CallBackUri }
};
client.AllowedScopes = selectedApis.Where(a => a.apiValue == "true").Select(a => a.apiName).ToList();
//e.g : client.AllowedScopes = {"employee_api"};
_isRepository.SaveClient(client, userApp);
}
Api Project Startup
services.AddAuthentication("Bearer").AddJwtBearer(opt => {
opt.Authority = "http://localhost:5000";
opt.Audience = "employee_api";
opt.RequireHttpsMetadata = false;
});
Api Sample Controller
[Authorize]
[Route("api/[controller]")]
[ApiController]
public class EmployeeController : BaseController
{
private readonly IEmployeeRepository _employeeRepoistory;
public EmployeeController(IServiceProvider provider, IEmployeeRepository employeeRepository) : base(provider)
{
_employeeRepoistory = employeeRepository;
}
[HttpGet]
public IActionResult GetEmployees([FromQuery] EmployeeResourceParameter parameter)
{
return Ok(_mapper.Map<IEnumerable<EmployeeModel>>(_employeeRepoistory.GetAll(parameter)));
}
[HttpGet("{id:int}")]
public IActionResult GetEmployeeById(int id)
{
var emp = _employeeRepoistory.GetById(id);
return Ok(_mapper.Map<EmployeeModel>(emp));
}
}
What I want is if a developer choose employee_api, they should just reach to EmployeeController's endpoints. However right now, they can reach all the apis no matter of what their choices are.
What are the steps to take for this on api side or auth server side?
Finally I get it done.. First of all, I realized that it is important to grasp the relation between ApiResource -> Scopes, Clients -> AllowedScopes. I suggest you to read the parts about them in the docs and here
When a client is registered to identityserver and then choose the api endpoints (eg: organization, employee, calender) they should be registered as allowedScopes of client (they live in ClientScopes table), I was doing it in right way. What I was doing wrong is I suppose all these scopes are ApiResources (for my case, because all my apis are living in the same host which I call it as CommonServiceApi, just one web api app). I redefined my ApiResources and its Scopes, as follows;
new ApiResource("commonserviceapi", "Common Service API")
{
Scopes = {
new Scope("calender_api", "Calender Api"),
new Scope("employee_api", "Employee Api"),
new Scope("organization_api", "Organization Api"),
}
}
On the api side, endpoints should be authorized with policies as indicated here.
Within the access token, allowed scopes of the requesting client are passed to the api app, so api grants accesses according to these values.
So Api Startup
services.AddAuthentication("Bearer").AddJwtBearer(opt =>
{
opt.Authority = "http://localhost:5000";
opt.Audience = "commonserviceapi";
opt.RequireHttpsMetadata = false;
});
services.AddAuthorization(options =>
{
options.AddPolicy("ApiEmployee", builder =>
{
builder.RequireScope("employee_api");
});
options.AddPolicy("ApiOrganization", builder =>
{
builder.RequireScope("organization_api");
});
});
And Api Controllers
[Authorize(Policy = "ApiEmployee")]
[Route("api/[controller]")]
[ApiController]
public class EmployeeController : BaseController
{
...
RequireScope is an extension method of IdentityServer4.AccessTokenValidation package by the way. You should include this package to your api project.
And lastly, this was a confusing point for me; while requesting an access token from server, scope parameter should be empty, as identityserver takes it from client's allowdScopes values. Almost all samples were filling this field, so you'd think it should be filled.
Related
I would like to use API key to access secured ServiceStack web service simply as possible:
I do not want to be able to register an user
I do not need user permissions or roles
Custom API key permissions would be a plus:
Be able to limit some service to a specific API key.
API keys will be managed directly from the database
What are the classes or methods I need to override? There are many extension points but I do not know what to keep and what to rewrite:
OrmLiteAuthRepository (base?)
ApiKeyAuthProvider
AuthUserSession
I am able to call a service with Bearer token (API key). It returns 200 Forbidden.
ApiKeyAuthProvider.AuthenticateAsync():
// authRepo is ServiceStack.Auth.OrmLiteAuthRepositoryMultitenancy
var userAuth = await authRepo.GetUserAuthAsync(apiKey.UserAuthId, token).ConfigAwait();
userAuth is NULL and this will throw this exception:
throw HttpError.Unauthorized(ErrorMessages.UserForApiKeyDoesNotExist.Localize(authService.Request));
I store my API keys at the 'ApiKey' table in SQL database:
public override void Configure(Container container)
{
string connectionString = GetConnectionStringByName("Main");
// Create and register an OrmLite DB Factory configured to use Live DB by default
var dbFactory = new OrmLiteConnectionFactory(connectionString, SqlServerDialect.Provider);
container.Register(dbFactory);
// Tell ServiceStack you want to persist User Auth Info in SQL Server
container.Register<IAuthRepository>(c => new OrmLiteAuthRepository(dbFactory) { UseDistinctRoleTables = true });
// It’s safe to always call this in your AppHost as it’s just ignored if you already have the tables created
container.Resolve<IAuthRepository>().InitSchema();
Plugins.Add(new AuthFeature(
() => new AuthUserSession(),
new IAuthProvider[]
{
new ApiKeyAuthProvider(AppSettings) {RequireSecureConnection = false}
}));
}
The API Key AuthProvider may not suit your use-case as it's designed to generate API Keys for registered users to provide an alternative way for them to call protected APIs.
To be able to model this using ServiceStack's built-in Auth API Key Auth Providers I would still have a registered AuthProvider and users representing the client that would use the API Keys.
But instead of providing User registration functionality, add them into the database manually then Generating API Keys for Existing Users.
You'll need to configure your preferred RDBMS to store the API Keys and Users in:
[assembly: HostingStartup(typeof(MyApp.ConfigureDb))]
public class ConfigureDb : IHostingStartup
{
public void Configure(IWebHostBuilder builder) => builder
.ConfigureServices((context, services) =>
services.AddSingleton<IDbConnectionFactory>(
new OrmLiteConnectionFactory(
context.Configuration.GetConnectionString("DefaultConnection"),
SqliteDialect.Provider)));
}
Configure ServiceStack's Auth Feature configured with the API Key AuthProvider:
[assembly: HostingStartup(typeof(MyApp.ConfigureAuth))]
public class ConfigureAuth : IHostingStartup
{
public void Configure(IWebHostBuilder builder) => builder
.ConfigureAppHost(appHost =>
{
appHost.Plugins.Add(new AuthFeature(() => new AuthUserSession(),
new IAuthProvider[] {
new ApiKeyAuthProvider(appHost.AppSettings) {
RequireSecureConnection = false,
SessionCacheDuration = TimeSpan.FromMinutes(10),
}
}));
});
}
Then configure an RDBMS OrmLiteAuthRepository pre-populated with the clients you want to allow access to, then generate any missing API Keys for them on startup:
[assembly: HostingStartup(typeof(MyApp.ConfigureAuthRepository))]
public class ConfigureAuthRepository : IHostingStartup
{
public void Configure(IWebHostBuilder builder) => builder
.ConfigureServices(services => services.AddSingleton<IAuthRepository>(c =>
new OrmLiteAuthRepository(c.Resolve<IDbConnectionFactory>())))
.ConfigureAppHost(appHost => {
var authRepo = appHost.Resolve<IAuthRepository>();
authRepo.InitSchema();
CreateUser(authRepo, "admin#email.com", "Admin User", "p#55wOrd",
roles: new[] { RoleNames.Admin });
CreateUser(authRepo, "admin.client#email.com", "Client Admin", "p#55wOrd",
roles: new[] { "ClientAdmin", "Client" });
CreateUser(authRepo, "client#email.com", "Client User", "p#55wOrd",
roles: new[] { "Client" });
},
afterAppHostInit: appHost => {
var authProvider = (ApiKeyAuthProvider)
AuthenticateService.GetAuthProvider(ApiKeyAuthProvider.Name);
using var db = appHost.TryResolve<IDbConnectionFactory>().Open();
var userWithKeysIds = db.Column<string>(db.From<ApiKey>()
.SelectDistinct(x => x.UserAuthId)).Map(int.Parse);
// Use custom UserAuth if configured
var userIdsMissingKeys = db.Column<string>(db.From<UserAuth>()
.Where(x => userWithKeysIds.Count == 0 || !userWithKeysIds.Contains(x.Id))
.Select(x => x.Id));
var authRepo = (IManageApiKeys)appHost.TryResolve<IAuthRepository>();
foreach (var userId in userIdsMissingKeys)
{
var apiKeys = authProvider.GenerateNewApiKeys(userId);
authRepo.StoreAll(apiKeys);
}
});
// Add initial Users to the configured Auth Repository
public void CreateUser(IAuthRepository authRepo, string email, string name, string password, string[] roles)
{
if (authRepo.GetUserAuthByUserName(email) == null)
{
var newAdmin = new AppUser { Email = email, DisplayName = name };
var user = authRepo.CreateUserAuth(newAdmin, password);
authRepo.AssignRoles(user, roles);
}
}
}
This will let you protect access to different APIs using role-based auth:
[ValidateIsAdmin]
public class AdminOnly { ... }
[ValidateHasRole("ClientAdmin")]
public class ClientAdminOnly { ... }
[ValidateHasRole("Client")]
public class AnyClient { ... }
Note: The Admin is a super user role that can access any protected API
If you don't want all these Auth components for your App you'll have to create your own Custom Auth Provider that implements its own Authentication which doesn't need to use any other components as it has full control over how the request is authenticated.
You can refer to the existing ApiKeyAuthProvider.cs for a guide on how to implement an API Key IAuthWithRequest Auth Provider that validates the BearerToken in its PreAuthenticateAsync() method.
I am using Blazor WASM with AzureB2C to call an API hosted in Azure Functions. I would like to call my API on a successful login to add/update user info into a database. I have been following this guide. When trying to inject my typed httpclient into the AccountClaimsPrincipalFactory I am met with a runtime error:
Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100]
Unhandled exception rendering component: ValueFactory attempted to access the Value property of this instance.
System.InvalidOperationException: ValueFactory attempted to access the Value property of this instance.
This shows in the browser, but the app compiles and runs just fine. The codes works great if I don't inject my PlatformServiceClient, but I need to make the API call to record the user. The following files are involved. I adjusted some things to simplify. This seems like the appropriate approach, but I have not seen examples where an api call was made in the claims factory.
CustomAccountFactory.cs
public class CustomAccountFactory: AccountClaimsPrincipalFactory<CustomUserAccount>
{
public IPlatformServiceClient client { get; set; }
public CustomAccountFactory(NavigationManager navigationManager,
IPlatformServiceClient platformServiceClient,
IAccessTokenProviderAccessor accessor) : base(accessor)
{
client = platformServiceClient;
}
public override async ValueTask<ClaimsPrincipal> CreateUserAsync(
CustomUserAccount account, RemoteAuthenticationUserOptions options)
{
var initialUser = await base.CreateUserAsync(account, options);
if (initialUser.Identity.IsAuthenticated)
{
//call the API here
await client.RegisterUserAsync();
}
return initialUser;
}
}
Program.cs excerpt
builder.Services.AddScoped<CustomAuthorizationMessageHandler>();
builder.Services.AddHttpClient<IPlatformServiceClient, PlatformServiceClient>(
client => client.BaseAddress = new Uri(builder.Configuration["PlatformServiceUrl"]))
.AddHttpMessageHandler<CustomAuthorizationMessageHandler>();
builder.Services.AddMsalAuthentication<RemoteAuthenticationState, CustomUserAccount>(options =>
{
builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
options.ProviderOptions.DefaultAccessTokenScopes.Add("openid");
options.ProviderOptions.DefaultAccessTokenScopes.Add("offline_access");
options.ProviderOptions.DefaultAccessTokenScopes.Add("access_as_user");
options.ProviderOptions.LoginMode = "redirect";
options.UserOptions.RoleClaim = "roles";
}).AddAccountClaimsPrincipalFactory<RemoteAuthenticationState, CustomUserAccount, CustomAccountFactory>();
CustomAuthorizationMessageHandler.cs
public class CustomAuthorizationMessageHandler : AuthorizationMessageHandler
{
public CustomAuthorizationMessageHandler(IAccessTokenProvider provider,
NavigationManager navigationManager)
: base(provider, navigationManager)
{
ConfigureHandler(
authorizedUrls: new[] { "http://localhost:7071" },
scopes: new[] { "access_as_user" });
}
}
I solved this by creating a named instance of the client and passing an IHttpClientFactory into the CustomAccountFactory.
builder.Services.AddHttpClient<PlatformServiceClient>("PlatformServiceClient",
client => client.BaseAddress = new Uri(builder.Configuration["PlatformServiceUrl"]))
.AddHttpMessageHandler<CustomAuthorizationMessageHandler>();
There I can create a client, but I have to setup my urls manually vs using the typed client where I have this work already done.
var client = factory.CreateClient("PlatformServiceClient");
var response = await client.GetAsync("/user/me");
I also registered the new client prior to calling AddMsalAuthenication
builder.Services.AddTransient(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("PlatformServiceClient"));
I did all of this following the code found here by Coding Flamingo. It is all working as expected.
.Net framework 4.6.1, class library project (web API)
I have added the swagger/swashbuckle nuget to the project, and added the SwaggerConfig.cs file to my App_Start folder.
Snip of SwaggerConfig.cs
using System.Web.Http;
using WebActivatorEx;
using MyService;
using Swashbuckle.Application;
[assembly: PreApplicationStartMethod(typeof(SwaggerConfig), "Register")]
I then go on and register the service
public class SwaggerConfig
{
public static void Register()
{
var thisAssembly = typeof(SwaggerConfig).Assembly;
GlobalConfiguration.Configuration
.EnableSwagger(c =>
{
c.SingleApiVersion("v1", "My API");
c.IncludeXmlComments(string.Format(#"{0}\swaggerdoc.XML",
System.AppDomain.CurrentDomain.BaseDirectory));
c.BasicAuth("basicauth").Description("Basic HTTP Authentication");
})
.EnableSwaggerUi(c =>
{
});
}
}
But I am not sure where I set the username/password required to view the documentation. The API methods all use a token for authentication, but I am trying to add a layer of security to stop a random user stumbling across the API documentation, by using basic auth.
If you want to secure the documentation, you have to to this on the webserver itself, with .net 4.x I assume IIS.
The method you are using is intended to tell Swagger to show a username/password logon form to call the service endpoints using these credentials with a basic HTTP authorization header.
To secure your swagger doc's using your basic authentication, you will need to enable it in the SwaggerConfig.cs file and couple it with a corresponding "security" property at the document or operation level.
Please note the below full comment from the SwaggerConfig.cs for Enabling Basic Authentication:
// You can use "BasicAuth", "ApiKey" or "OAuth2" options to describe security schemes for the API.
// See https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md for more details.
// NOTE: These only define the schemes and need to be coupled with a corresponding "security" property
// at the document or operation level to indicate which schemes are required for an operation. To do this,
// you'll need to implement a custom IDocumentFilter and/or IOperationFilter to set these properties
// according to your specific authorization implementation
//
c.BasicAuth("basic").Description("Basic HTTP Authentication");
how to couple it with the corresponding "security" property? You can add a class to implement that filter as:
public class SwaggerHeaderFilter : IOperationFilter
{
public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
{
var filterPipeline = apiDescription.ActionDescriptor.GetFilterPipeline();
// check if authorization is required
var isAuthorized = filterPipeline
.Select(filterInfo => filterInfo.Instance)
.Any(filter => filter is IAuthorizationFilter);
// check if anonymous access is allowed
var allowAnonymous = apiDescription.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Any();
if (isAuthorized && !allowAnonymous)
{
if (operation.security == null)
operation.security = new List<IDictionary<string, IEnumerable<string>>>();
var auth = new Dictionary<string, IEnumerable<string>>
{
{"basic", Enumerable.Empty<string>()}
};
operation.security.Add(auth);
}
}
}
and in the swaggerConfig.cs file, add this to the configurations:
c.OperationFilter<SwaggerHeaderFilter>();
And Don't forget to decorate your Api's with the [Authorize] tag
reference:
https://codingsight.com/swashbuckle-swagger-configuration-for-webapi/
I'm using Identity Server 3 to authenticate and generate Access/Refresh tokens for my angular Client.
I'm currently setting the Refresh Token to expire in 48 hours for my Angular Client.
Some users who use my Angular application will need to be Signed On for 100 days straight without having to re-enter their credentials, is it possible to set the expiration of my Refresh Token for a specific user only instead of the entire client?
I have 100 users in my database, I want just one specific user to not need to re-authenticate in 100 days while the rest should authenticate every 48 hours.
Something along the lines of:
if (user == "Super Man") {
AbsoluteRefreshTokenLifetime = TimeSpan.FromDays(100.0).Seconds,
}
Is this possible to achieve? or am I restricted to only setting the Refresh Token Expiration for the Entire Client?
Thank You
I've never worked with IdentityServer3 and I didn't test the code below, but I think the concept may work.
When I take a look at the code of IdentityServer3 then I can see that in DefaultRefreshTokenService.CreateRefreshTokenAsync the lifetime is set:
int lifetime;
if (client.RefreshTokenExpiration == TokenExpiration.Absolute)
{
Logger.Debug("Setting an absolute lifetime: " + client.AbsoluteRefreshTokenLifetime);
lifetime = client.AbsoluteRefreshTokenLifetime;
}
else
{
Logger.Debug("Setting a sliding lifetime: " + client.SlidingRefreshTokenLifetime);
lifetime = client.SlidingRefreshTokenLifetime;
}
You wouldn't want to change the core code, but you should be able to override the IRefreshTokenService with your own implementation.
When I take the code from CustomUserService sample as example:
internal class Startup
{
public void Configuration(IAppBuilder app)
{
app.Map("/core", coreApp =>
{
var factory = new IdentityServerServiceFactory()
.UseInMemoryClients(Clients.Get())
.UseInMemoryScopes(Scopes.Get());
var refreshTokenService = new MyDefaultRefreshTokenService();
// note: for the sample this registration is a singletone (not what you want in production probably)
factory.RefreshTokenService = new Registration<IrefreshTokenService>(resolver => refreshTokenService);
Where MyDefaultRefreshTokenService is a copy of the DefaultRefreshTokenService.
In order to make it compile add a NuGet package of IdentityModel (v1.13.1) and add the following class:
using System;
namespace IdentityServer3.Core.Extensions
{
internal static class DateTimeOffsetHelper
{
internal static Func<DateTimeOffset> UtcNowFunc = () => DateTimeOffset.UtcNow;
internal static DateTimeOffset UtcNow
{
get
{
return UtcNowFunc();
}
}
internal static int GetLifetimeInSeconds(this DateTimeOffset creationTime)
{
return (int)(UtcNow - creationTime).TotalSeconds;
}
}
}
Now there are some compilation errors concerning the events. You can remove the events in order to test the code. If it works you can always choose to add them.
And now for the implementation of the RefreshTokenLifetime per user. In your version of the RefreshTokenService you can remove the client code and use your own logic to determine the lifetime per user.
The subject is available, though I don't know if it already contains enough information. But if it does then you can access the userManager to read the lifetime from the store. Or use an alternative to pass the lifetime information (perhaps you can use a claim containing the lifetime value).
Again, I didn't test this, but I think the concept should work.
Considerations
Consider sliding sessions for example. With sliding sessions, you would send a new short-lived token with every authenticated action made by the user. As long as the user is active he will stay authenticated (e.g. it requires user interaction before expiration interval, although it requires token management implementations). If the user sends an expired token, it means he has been inactive for a while.
Let's see how JWT works:
The JWT is mainly suitable for the following cases:
In case of building API services that need to support
server-to-server or client-to-server (like a mobile app or single page app (SPA)) communication, using JWTs as your API tokens is a
very smart idea (clients will be making requests frequently, with
limited scope, and usually authentication data can be persisted in a
stateless way without too much dependence on user data).
If you’re building any type of service where you need three or more
parties involved in a request, JWTs can also be useful.
if you’re using user federation (things like single sign-on and
OpenID Connect), JWTs become important because you need a way to
validate a user’s identity via a third party.
more clarification at stop using jwts as session tokens
So Stop using JWT for sessions, it’s a bad idea to use JWTs as session tokens for most of cases.
Possible Solution
For Refreshing JWT, the JWT refresh tokens and .NET Core may be useful to implement your own code And descriptions inside JWT (JSON Web Token) automatic prolongation of expiration guides you to design a working scenario. You need to inspect desired user before refreshing operation.
I found another implementation at Handle Refresh Token Using ASP.NET Core 2.0 And JSON Web Token for you, maybe useful.
I'm not familiar with Microsoft's Identity Server (the "Identity Service" I refer to in the code below is a custom implementation), but you could consider writing an authentication handler to intercept the token in HTTP headers, examine a token prefix, then decide whether to process normally or allow an extended lifetime.
In my case, I intercept the token prior to JWT processing it. (I had to do this to get around a SharePoint workflow limitation. Oh, SharePoint.) Here's the AuthenticationHandler class:
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
namespace CompanyName.Core2.Application.Middleware
{
[UsedImplicitly]
public class AuthenticationHandler : AuthenticationHandler<AuthenticationOptions>
{
public const string AuthenticationScheme = "CompanyName Token";
[UsedImplicitly] public const string HttpHeaderName = "Authorization";
[UsedImplicitly] public const string TokenPrefix = "CompanyName ";
public AuthenticationHandler(IOptionsMonitor<AuthenticationOptions> Options, ILoggerFactory Logger, UrlEncoder Encoder, ISystemClock Clock)
: base(Options, Logger, Encoder, Clock)
{
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!Request.Headers.TryGetValue(HttpHeaderName, out StringValues authorizationValues))
{
// Indicate failure.
return await Task.FromResult(AuthenticateResult.Fail($"{HttpHeaderName} header not found."));
}
string token = authorizationValues.ToString();
foreach (AuthenticationIdentity authenticationIdentity in Options.Identities)
{
if (token == $"{TokenPrefix}{authenticationIdentity.Token}")
{
// Authorization token is valid.
// Create claims identity, add roles, and add claims.
ClaimsIdentity claimsIdentity = new ClaimsIdentity(AuthenticationScheme);
claimsIdentity.AddClaim(new Claim(ClaimTypes.Name, authenticationIdentity.Username));
foreach (string role in authenticationIdentity.Roles)
{
claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, role));
}
foreach (string claimType in authenticationIdentity.Claims.Keys)
{
string claimValue = authenticationIdentity.Claims[claimType];
claimsIdentity.AddClaim(new Claim(claimType, claimValue));
}
// Create authentication ticket and indicate success.
AuthenticationTicket authenticationTicket = new AuthenticationTicket(new ClaimsPrincipal(claimsIdentity), Scheme.Name);
return await Task.FromResult(AuthenticateResult.Success(authenticationTicket));
}
}
// Indicate failure.
return await Task.FromResult(AuthenticateResult.Fail($"Invalid {HttpHeaderName} header."));
}
}
}
Then in the Startup class of your service, add code to decide which authentication handler to use. The key feature here is the ForwardDefaultSelector:
public void ConfigureServices(IServiceCollection Services)
{
// Require authentication token.
// Enable CompanyName token for SharePoint workflow client, which cannot pass HTTP headers > 255 characters (JWT tokens are > 255 characters).
// Enable JWT token for all other clients. The JWT token specifies the security algorithm used when it was signed (by Identity service).
Services.AddAuthentication(AuthenticationHandler.AuthenticationScheme).AddCompanyNameAuthentication(Options =>
{
Options.Identities = Program.AppSettings.AuthenticationIdentities;
Options.ForwardDefaultSelector = HttpContext =>
{
// Forward to JWT authentication if CompanyName token is not present.
string token = string.Empty;
if (HttpContext.Request.Headers.TryGetValue(AuthenticationHandler.HttpHeaderName, out StringValues authorizationValues))
{
token = authorizationValues.ToString();
}
return token.StartsWith(AuthenticationHandler.TokenPrefix)
? AuthenticationHandler.AuthenticationScheme
: JwtBearerDefaults.AuthenticationScheme;
};
})
.AddJwtBearer(Options =>
{
Options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Program.AppSettings.ServiceOptions.TokenSecret)),
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(_clockSkewMinutes)
};
});
Add an extension method to the AuthenticationBuilder class:
public static AuthenticationBuilder AddCompanyNameAuthentication(this AuthenticationBuilder AuthenticationBuilder, Action<AuthenticationOptions> ConfigureOptions = null)
{
return AuthenticationBuilder.AddScheme<AuthenticationOptions, AuthenticationHandler>(AuthenticationHandler.AuthenticationScheme, ConfigureOptions);
}
And authentication options if you need them.
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authentication;
namespace CompanyName.Core2.Application.Middleware
{
public class AuthenticationOptions : AuthenticationSchemeOptions
{
[UsedImplicitly]
public AuthenticationIdentities Identities { get; [UsedImplicitly] set; }
public AuthenticationOptions()
{
Identities = new AuthenticationIdentities();
}
}
}
AuthenticationIdentities is just a class I define to associate a token with a username, roles, and claims (the token for the SharePoint workflow engine). It's populated from appsettings.json. Your options class most likely would contain a list of users who are authorized for an extended lifetime.
using System.Collections.Generic;
using JetBrains.Annotations;
namespace CompanyName.Core2.Application.Middleware
{
public class AuthenticationIdentity
{
public string Token { get; [UsedImplicitly] set; }
public string Username { get; [UsedImplicitly] set; }
[UsedImplicitly] public List<string> Roles { get; [UsedImplicitly] set; }
[UsedImplicitly] public Dictionary<string, string> Claims { get; [UsedImplicitly] set; }
public AuthenticationIdentity()
{
Roles = new List<string>();
Claims = new Dictionary<string, string>();
}
}
}
I create a ASP.NET WebApi project, it use [Authorize] attribute to authorize controllers, such as:
[System.Web.Http.Authorize(Roles="Users, Admins")]
public ValueController : ApiController
{
[HttpGet]
public string GetValue()
{
return "Hello World !";
}
}
now I want to add Signalr to this project. This is what I did:
In App_Start/Startup.Auth.cs, add following codes in ConfigureAuth() function to start SignalR.
app.Map(signalr, map =
{
map.UseCors(CorsOptions.AllowAll);
var hubConfiguration = new HubConfiguration
{
};
hubConfiguration.EnableDetailedErrors = true;
map.RunSignalR(hubConfiguration);
});
Then I add a MessageHub, trying to allow authorized user to send messages to all clients:
[Microsoft.AspNet.SignalR.Authorize]
public class MessageHub : Hub
{
public void SendMessage(string message)
{
Clients.All.SendMessage(message);
}
}
if I start this project, type http://localhost:4000/signalr/hubs in browser, I can receive a hubs js.
Q1 - For me that means SignalR is working on my WebApi project, right?
now I need to test SendMessage, first thing I did is login through http://localhost:4000/Token, then get an access_token. In webApi I usually put this token in Header:
Authorization : Bearer access_token
to access APIs.
Q2 - now what should I do with this token to access MessageHub?