I have built an API that controls some smart home stuff. To prevent the whole internet from doing so, I added authentication using JWT / Bearer. The API contains endpoints for the smart home stuff aswell as some user management:
API endpoints for users
The login will return a JWT token if credentials were valid. It is also built using .NET 6:
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(x =>
{
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"])),
};
});
Login Controller:
[HttpPost]
public async Task<IActionResult> Login([FromBody] UserLogin login)
{
var user = await _userService.GetUser(login.Username);
if (user is not null && _userService.IsPasswordCorrect(user, login.Password))
{
var tokens = await _userService.GetJwtAndRefreshToken(user);
return Ok(new LoginResponse { JWT = tokens.Jwt, RefreshToken = tokens.Refreshtoken });
}
return Unauthorized("Wrong username or password!");
}
Now I am trying to build a frontend for this app using blazor. When creating the app, i used the option "individual user accounts" for authentication. It is documented here: https://learn.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/standalone-with-authentication-library?view=aspnetcore-6.0&tabs=visual-studio
This created the following in the blazow WASM app:
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddOidcAuthentication(options =>
{
// Configure your authentication provider options here.
// For more information, see https://aka.ms/blazor-standalone-auth
builder.Configuration.Bind("Local", options.ProviderOptions);
});
await builder.Build().RunAsync();
appsettings.json looks like this:
{
"Local": {
"Authority": "https://localhost:7110/login",
"ClientId": "33333333-3333-3333-33333333333333333"
}
}
I changed the Authority to my login api url, but doesn't seem to be enough.
Clicking on the login button that was added by default fires this request:
Request
Is there a simple way to use the MS Authorization framework with my custom api?
I spent great amount of time on this. These are my notes from it. Note that I am using IdentityServer. Probably a lot of stuff will be different for you. But it should at least guide you what to check.
It works (for me), but best-practise is not garantee.
My API address is on port 5001, Client is on port 5101
For Client project
Change HttpClient address in Client. Change Http MessageHandler. Change address for public client
var clientBaseAddress = new Uri(builder.Configuration["apiurl"] ?? throw new ArgumentNullException("apirul is null (reading from config file)"));
builder.Services.AddHttpClient("BlazorApp6.ServerAPI", client =>client.BaseAddress = clientBaseAddress)
.AddHttpMessageHandler(sp =>
{//this is need when api is separated. https://code-maze.com/using-access-token-with-blazor-webassembly-httpclient/
var handler = sp.GetService<AuthorizationMessageHandler>()!
.ConfigureHandler(
authorizedUrls: new[] { builder.Configuration["HttpMessageHandlerAuthorizedUrls"] },
scopes: new[] { "BlazorApp6.ServerAPI" }
);
return handler;
});
builder.Services.AddHttpClient<PublicClient>(client => client.BaseAddress = clientBaseAddress);
Add HttpMessageHandlerAuthorizedUrls apiurl to appsettings (example for developement):
"apiurl": "https://localhost:5001",
"HttpMessageHandlerAuthorizedUrls": "https://localhost:5001",
Program.cs AddApiAuthorization is different (set opt.ProviderOptions.ConfigurationEndpoint)
builder.Services.AddApiAuthorization(
//this line is only when address of api consumer is different
opt => opt.ProviderOptions.ConfigurationEndpoint = builder.Configuration["ApiAuthorizationConfigurationEndpoint"]
).AddAccountClaimsPrincipalFactory<CustomUserFactory>();
Add ApiAuthorizationConfigurationEndpoint to appsettings
"ApiAuthorizationConfigurationEndpoint": "https://localhost:5001/_configuration/BlazorApp6.Client"
Change launchSetting to different port
"applicationUrl": "https://localhost:5101;http://localhost:5100",
For api project
Add cors to client app
string developmentCorsPolicy = "dev_cors";
services.AddCors(opt =>
{
opt.AddPolicy(name: developmentCorsPolicy, builder =>
{
builder.WithOrigins("https://localhost:5101", "https://localhost:5201")
.WithMethods("GET", "POST", "PUT", "DELETE")
.AllowAnyHeader();
});
});
//...
if (app.Environment.IsDevelopment())
app.UseCors(developmentCorsPolicy);
There is probably some need to add cors for identiy server, but it works without it.
in case it is needed:
services.AddSingleton<ICorsPolicyService>((container) =>
{
var logger = container.GetRequiredService<ILogger<DefaultCorsPolicyService>>();
return new DefaultCorsPolicyService(logger)
{
AllowAll = true
};
});
Change appsettings IdentityServer section to have some info about client.
This info is obtained in OidcController with requests starting _configuration:
"IdentityServer": {
"Clients": {
"BlazorApp6.Client": {
"Profile": "SPA",
"LogoutUri": "https://localhost:5101/authentication/logout-callback",
"RedirectUri": "https://localhost:5101/authentication/login-callback"
},
},
"Key": {
"Type": "Development"
} }
Note that Profile has changed to SPA (instead of IdentityServerSPA, which means hosted)
Related
I have a Hub that serves up near real-time data. I want the provider of the real-time data to be able to send messages to all connected clients. The problem I am running into is authenticating the connection because the provider is a Windows Service. So, I am creating a client using HubConnectionBuilder. When I attempt to connect, I am getting a 401 authentication failure which is expected.
I am trying to figure out how to use the existing Identity authentication that exists in the shared Entity Framework database. Is there a way to get an authentication token using UserManager or SigninManager from this stand-alone service? Or is there a better approach to allow the HubConnection to authenticate?
Here is the connection code I am using:
var notifyConnection = new HubConnectionBuilder()
.WithUrl("https://localhost:5001/notify", options =>
{
options.AccessTokenProvider = () => Task.FromResult(_myAccessToken);
})
.WithAutomaticReconnect()
.Build();
notifyConnection.Closed += async (error) =>
{
if (Debug) _logger.LogInformation("Hub connection closed: " + error.Message);
await Task.Delay(new Random().Next(0, 5) * 1000);
await notifyConnection.StartAsync();
};
try
{
var task = notifyConnection.StartAsync();
task.Wait();
}
catch (Exception ex)
{
if (Debug) _logger.LogInformation("Hub connection start error: " + ex.Message);
}
_myAccessToken is not defined. I am trying to figure out how to get a valid access token that will authenticate properly.
EDIT:
I have tried adding JWT authentication. I can verify that the bearer token is getting generated properly. But it doesn't appear to be validated on the Hub.
I have added the following to the services configuration:
var key = new SymmetricSecurityKey(System.Text.Encoding.ASCII.GetBytes(Configuration["JwtKey"]));
services.AddAuthentication().AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
LifetimeValidator = (before, expires, token, parameters) =>
expires > DateTime.UtcNow,
ValidateAudience = false,
ValidateIssuer = false,
ValidateActor = false,
ValidateLifetime = true,
IssuerSigningKey = key,
NameClaimType = ClaimTypes.NameIdentifier
};
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
if (!string.IsNullOrEmpty(accessToken))
{
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.AddPolicy(JwtBearerDefaults.AuthenticationScheme, policy =>
{
policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);
policy.RequireClaim(ClaimTypes.NameIdentifier);
});
});
I have also updated the HubConnectionBuilder to use:
notifyConnection = new HubConnectionBuilder()
.WithUrl(baseUrl + "/notify", options =>
{
options.AccessTokenProvider = async () =>
{
var stringData = JsonConvert.SerializeObject(new { username = "****", password = "****" });
var content = new StringContent(stringData);
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
var response = await httpClient.PostAsync(baseUrl + "/api/token", content);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
};
})
.WithAutomaticReconnect()
.Build();
It still returns a 401. When I tried to force the Authorize attribute to:
[Authorize(AuthenticationSchemes = "Bearer,Cookies")]
It then returns a 500 error.
So, after changing the Authorize attribute on the Hub class, I was able to get JWT authentication working along with the built-in Identity authentication.
Here is the new Authorize attribute:
[Authorize(AuthenticationSchemes = "Bearer,Identity.Application")]
I added a simple token generation API controller to feed the HubConnection. The following sources were very helpful:
https://www.codemag.com/article/1807061/Build-Real-time-Applications-with-ASP.NET-Core-SignalR
.Net SignalR use JWT Bearer Authentication when Cookie Authentication is also configured
I am using aspnet core 5.0 webapi with CQRS in my project and already have jwt implementation. Not using role management from aspnet core but manually added for aspnet users table role field and it is using everywhere. In internet I can't find any article to implement keycloak for existing authentication and authorization. My point is for now users login with their email+password, idea is not for all but for some users which they already stored in keycloak, or for some users we will store there, give option login to our app using keycloak as well.
Scenario 1:
I have admin#gmail.com in both in my db and in keycloak and both are they in admin role, I need give access for both to login my app, first scenario already working needs implement 2nd scenarion beside first.
Found only this article which implements securing app (as we have already and not trying to replace but extend)
Medium keycloak
My jwt configuration looks like:
public static IServiceCollection AddCustomAuthentication(this IServiceCollection services,
IConfiguration configuration)
{
var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(configuration.GetSection("AppSettings:Token").Value));
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(opt =>
{
opt.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = key,
ValidateAudience = false,
ValidateIssuer = false,
ClockSkew = TimeSpan.Zero
};
opt.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
{
context.Response.Headers.Add("Token-Expired", "true");
}
return Task.CompletedTask;
}
};
});
return services;
}
My jwt service looks like:
public JwtGenerator(IConfiguration config)
{
_key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config.GetSection("AppSettings:Token").Value));
}
public string CreateToken(User user)
{
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, user.Id),
new(ClaimTypes.Email, user.Email),
new(ClaimTypes.Name, user.UserName),
new(ClaimTypes.Role, user.Role.ToString("G").ToLower())
};
var creds = new SigningCredentials(_key, SecurityAlgorithms.HmacSha512);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.UtcNow.AddMinutes(15),
SigningCredentials = creds
};
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
My login method looks like:
public async Task<GetToken> Handle(LoginCommand request, CancellationToken cancellationToken)
{
var user = await _userManager.FindByEmailAsync(request.Email);
if (user == null)
throw new BadRequestException("User not found");
UserManagement.ForbiddenForLoginUser(user);
var result = await _signInManager.CheckPasswordSignInAsync(user, request.Password, false);
if (result.Succeeded)
{
user.IsRoleChanged = false;
RefreshToken refreshToken = new RefreshToken
{
Name = _jwtGenerator.GenerateRefreshToken(),
DeviceName = $"{user.UserName}---{_jwtGenerator.GenerateRefreshToken()}",
User = user,
Expiration = DateTime.UtcNow.AddHours(4)
};
await _context.RefreshTokens.AddAsync(refreshToken, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
return new GetToken(_jwtGenerator.CreateToken(user),refreshToken.Name);
}
throw new BadRequestException("Bad credentials");
}
My authorization handler:
public static IServiceCollection AddCustomMvc(this IServiceCollection services)
{
services.AddMvc(opt =>
{
var policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
opt.Filters.Add(new AuthorizeFilter(policy));
// Build the intermediate service provider
opt.Filters.Add<CustomAuthorizationAttribute>();
}).AddFluentValidation(cfg => cfg.RegisterValidatorsFromAssemblyContaining<CreateProjectCommand>());
return services;
}
What is best practise to implement keycloak authentiaction+authorization beside my current approach and give users to login with two scenarios, normal and keycloak login.
P.S. Ui is different and we are using angular this one just webapi for backend.
Since your login method returns a jwt, you could configure multiple bearer tokens by chaining .AddJwtBearer(), one for your normal login and one for keycloak.
Here is a link to a question that might solve your problem: Use multiple jwt bearer authentication.
Keycloak configuration:
Go to Roles -> Realm Roles and create a corresponding role.
Go to Clients -> Your client -> Mappers.
Create a new role mapper and select "User Realm Role" for Mapper Type, "roles" for Token Claim Name and "String" for Claim JSON Type. Without the mapping the role configured before would be nested somewhere else in the jwt.
You can use the debugger at jwt.io to check if your token is correct. The result should look like this:
{
"exp": 1627565901,
"iat": 1627564101,
"jti": "a99ccef1-afa9-4a62-965b-15e8d33de7de",
// [...]
// roles nested in realm_access :(
"realm_access": {
"roles": [
"offline_access",
"uma_authorization",
"Admin"
]
},
// [...]
// your mapped roles in your custom claim
"roles": [
"offline_access",
"uma_authorization",
"Admin"
]
// [...]
}
I have a Problem with Authenticating my Applications with IdentityServer4.
I have:
a IdentityServer4-Server to authenticate multiple Clients
(running on localhost:5000)
a Api which is mainly used for storing and retrieving data
with SignalR (running on localhost:5200)
a Web-Application to read & write Data from the Api and have
User specific interactions (running on localhost:80)
and a Desktop-Application to read Data from Api
What works:
Desktop User can use "ApiClient" to use Api with
Client-Credential-Flow.
Web User can use "WebClient" to get a Token with Implicit-Flow when
Logging in.
What doesnt work:
Web Client cant Authenticate on the Api using his Token from
Implicit-Flow, even though he has the scope for the Api.
ApiResources:
public static IEnumerable<ApiResource> Apis = new List<ApiResource>
{
new ApiResource("WebApplicationResource", ""),
new ApiResource("ApiServerResource", "")
};
Clients:
public static IEnumerable<Client> Clients = new List<Client>
{
// Web Application (SPA) Client
new Client
{
ClientId = "WebClient",
AllowedGrantTypes = GrantTypes.Implicit,
AllowAccessTokensViaBrowser = true,
RequireConsent = false,
RedirectUris = {
"http://localhost:3000/callback"
},
PostLogoutRedirectUris = {
"http://localhost:3000/post_logout"
},
AllowedCorsOrigins = {
"http://localhost:3000"
},
AllowedScopes = {
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"WebApplicationResource",
"WebApiResourceScope"
}
},
// Api (Signalr) Client
new Client
{
ClientId = "ApiClient",
AllowedGrantTypes = GrantTypes.ClientCredentials,
RequireConsent = false,
ClientSecrets = {
new Secret("SecretCredentialsValue".Sha256())
},
AllowedScopes = {
"WebApiResourceScope"
}
}
};
IS4 Server Authentication in Startup:
//in ConfigureServices
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", jwt =>
{
jwt.Authority = "http://localhost:5000";
jwt.Audience = "WebApplicationResource";
jwt.RequireHttpsMetadata = false;
});
...
//in Configure
app.UseIdentityServer();
Api Server adding IS4 Authentication in Startup:
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.ApiName = "WebApiResourceScope";
});
Api Server SignalR implementation in Startup:
//in ConfigureServices
services.AddSignalR()
...
//in Configure
app.UseSignalR(routes =>
{
routes.MapHub<EventHub>("/api");
});
SignalR Hub Authentication added with:
[Authorize(AuthenticationSchemes = "Bearer")]
Here is the output when testing:
Api showing that Desktop Client can Authenticate:
The Web Client after successfully logging in, trying to authenticate on the SignalR Server which is on the Api (like the Client did on top) with its bearer token.
Console Output when Error happening:
Output of Api, showing that Bearer Token was not authenticated:
Did I misunderstood something about the scopes? Why can i add multiple scopes on Web Client but only use it on one of the declared Api Resources?
Do I have to get 2 Tokens for the Web Client? If so, what should i use to get a Token, Client Credentials isn't recommended on Web to Web communication.
Or what is it that I am not understanding.
I have an ASP.NET MVC application for hosting an IdentityServer3, but I want to host Angular + WebAPI 2 custom administration app on that same host. That admin app is using oidc-client library for authentication. Below is my Startup class for configuring IdentityServer and calling UseIdentityServerBearerTokenAuthentication method. As you can see I called that method in a async Task because that happened to soon, before IdentityServer started.
Authentication works, my Angular ajax requests are filled with valid access tokens, but I dont get any Claims on WebApi controllers. ClaimsPrincipal have empty Claims list, and IsAuthenticated is false.
Also my client configuration is properly set. Is there something wrong with this setup?
public class Startup
{
public void Configuration(IAppBuilder app)
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Trace()
.CreateLogger();
var factory = new IdentityServerServiceFactory();
factory.Register<IdentityDatabaseModel>(new Registration<IdentityDatabaseModel>(typeof(IdentityDatabaseModel)));
factory.Register<UserDataService>(new Registration<UserDataService>(typeof(UserDataService)));
factory.Register<TokenDataService>(new Registration<TokenDataService>(typeof(TokenDataService)));
factory.Register<ClaimsDataService>(new Registration<ClaimsDataService>(typeof(ClaimsDataService)));
factory.Register<ClientDataService>(new Registration<ClientDataService>(typeof(ClientDataService)));
factory.UserService = new Registration<IUserService>(typeof(UserService));
factory.RefreshTokenStore = new Registration<IRefreshTokenStore, RefreshTokenStore>();
factory.ClientStore = new Registration<IClientStore, ClientStore>();
factory.UseInMemoryScopes(WebApplication1.Models.IS.Scopes.Get());
var options = new IdentityServerOptions
{
SigningCertificate = Certificate.Get(),
Factory = factory,
RequireSsl = false,
LoggingOptions = new LoggingOptions
{
//EnableHttpLogging = true,
EnableKatanaLogging = true,
EnableWebApiDiagnostics = true,
WebApiDiagnosticsIsVerbose = true
},
EnableWelcomePage = false
};
app.UseIdentityServer(options);
#region IdentityServer authentication
JwtSecurityTokenHandler.InboundClaimTypeMap.Clear();
Task.Factory.StartNew(() => {
System.Threading.Thread.Sleep(5000);
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "http://localhost:17343",
RequiredScopes = new[] { "openid", "email", "roles", "profile" },
ClientId = "lsidentity",
ClientSecret = "secret"
});
});
#endregion
}
}
The problem was that I needed to configure IssuerName and SigningCertificate in WebApi configuration, so it looks like this:
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "http://localhost:17343",
//Authority = "http://192.168.254.3:303",
RequiredScopes = new[] { "openid",
"email", "profile" },
IssuerName = "http://localhost:17343", //added this
SigningCertificate = Certificate.Get(), // ...and this
// client credentials for the introspection endpoint
ClientId = "lsidentity",
ClientSecret = "secret".Sha256()
});
There was an issue on github, but I did not find it at first.
https://github.com/IdentityServer/IdentityServer3.AccessTokenValidation/issues/38
There is also no need to call this as Task, it works fine now.
I am trying to combine OpenId authentication and bearer token authentication through the use of IdentityServer.
My project is an Asp.net MVC project, with a web api component. I would like to use OpenId authentication for the standard MVC controllers, and bearer token authentication for the web api component.
My front end is written in Angularjs, and calls web api methods through the $http service to populate the UI.
I am having a problem setting up the authentication workflow.
My thoughts initially were to try to execute the following algorithm:
User requests some web api method from the server through an ajax call ($http)
App detects the user is not authenticated and bearer token is not present in request header, redirects to MVC controller method to retrieve the bearer token.
Since the user is not authenticated, the server will redirect to the IdentityServer default login page.
Once the user is logged in, the server automatically redirects back to the bearer token method to get a bearer token
The generated bearer token is somehow returned to the client.
However my algorithm breaks down at step 3. I am trying to implement step 2 by using an httpinterceptor to inspect outgoing request to determine if a bearer token is present.
app.factory('authInterceptorService', ['$q', '$location', 'localStorageService', function ($q, $location, localStorageService) {
var authInterceptorServiceFactory = {};
var _request = function (config) {
config.headers = config.headers || {};
var authData = localStorageService.get('authorizationData');
if (authData) {
config.headers.Authorization = 'Bearer ' + authData.token;
}
else {
$location.path('/Authentication/BearerToken');
}
return config;
}
var _responseError = function (rejection) {
if (rejection.status === 401) {
$location.path('/Authentication/BearerToken');
}
return $q.reject(rejection);
}
authInterceptorServiceFactory.request = _request;
authInterceptorServiceFactory.responseError = _responseError;
return authInterceptorServiceFactory;}]);
If the token is not present, I exit the SPA and redirect to the bearer token MVC method.
This will successfully redirect to the login page, however once I log in, I am returned a bearer token in JSON directly on the page, since I am now outside of the Ajax call.
So my question is
could you please provide me with alternate ideas (and an outline of an implementation) on how to combine these two modes of authentication into one workflow?
Perhaps there is a way to customize IdentityServer to do what I want?
My basic requirement is that an unauthenticated user on my SPA angularjs app is redirected to the default IdentityServer login page, and once logged in, that the initial request be fulfilled.
Thanks in advance!
Just in case you are curious, my IdentityServer setup is as follows. The authentication service is in a separate project from the web application. Each has it's own Startup.cs file.
The MVC Startup.cs file
public class Startup
{
public void Configuration(IAppBuilder app)
{
AntiForgeryConfig.UniqueClaimTypeIdentifier = Thinktecture.IdentityServer.Core.Constants.ClaimTypes.Subject;
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies",
});
var openIdConfig = new OpenIdConnectAuthenticationOptions
{
Authority = "https://localhost:44301/identity",
ClientId = "baseballStats",
Scope = "openid profile roles baseballStatsApi",
RedirectUri = "https://localhost:44300/",
ResponseType = "id_token token",
SignInAsAuthenticationType = "Cookies",
UseTokenLifetime = false,
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = async n =>
{
var userInfoClient = new UserInfoClient(
new Uri(n.Options.Authority + "/connect/userinfo"),
n.ProtocolMessage.AccessToken);
var userInfo = await userInfoClient.GetAsync();
// create new identity and set name and role claim type
var nid = new ClaimsIdentity(
n.AuthenticationTicket.Identity.AuthenticationType,
Thinktecture.IdentityServer.Core.Constants.ClaimTypes.GivenName,
Thinktecture.IdentityServer.Core.Constants.ClaimTypes.Role);
userInfo.Claims.ToList().ForEach(c => nid.AddClaim(new Claim(c.Item1, c.Item2)));
// keep the id_token for logout
nid.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
// add access token for sample API
nid.AddClaim(new Claim("access_token", n.ProtocolMessage.AccessToken));
// keep track of access token expiration
nid.AddClaim(new Claim("expires_at", DateTimeOffset.Now.AddSeconds(int.Parse(n.ProtocolMessage.ExpiresIn)).ToString()));
// add some other app specific claim
nid.AddClaim(new Claim("app_specific", "some data"));
n.AuthenticationTicket = new AuthenticationTicket(
nid,
n.AuthenticationTicket.Properties);
}
}
};
app.UseOpenIdConnectAuthentication(openIdConfig);
app.UseResourceAuthorization(new AuthorizationManager());
app.Map("/api", inner =>
{
var bearerTokenOptions = new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "https://localhost:44301/identity",
RequiredScopes = new[] { "baseballStatsApi" }
};
inner.UseIdentityServerBearerTokenAuthentication(bearerTokenOptions);
var config = new HttpConfiguration();
config.MapHttpAttributeRoutes();
inner.UseWebApi(config);
});
}
}
The IdentityServer Startup.cs
public class Startup
{
public void Configuration(IAppBuilder app)
{
var policy = new System.Web.Cors.CorsPolicy
{
AllowAnyOrigin = true,
AllowAnyHeader = true,
AllowAnyMethod = true,
SupportsCredentials = true
};
//policy.ExposedHeaders.Add("location: http://location.com");
policy.Headers.Add("location: testing");
app.UseCors(new CorsOptions
{
PolicyProvider = new CorsPolicyProvider
{
PolicyResolver = context => Task.FromResult(policy)
}
});
app.Map("/identity", idsrvApp =>
{
idsrvApp.UseIdentityServer(new IdentityServerOptions
{
SiteName = "Embedded IdentityServer",
SigningCertificate = LoadCertificate(),
Factory = InMemoryFactory.Create(
users: Users.Get(),
clients: Clients.Get(),
scopes: Scopes.Get())
});
});
}
X509Certificate2 LoadCertificate()
{
return new X509Certificate2(
string.Format(#"{0}\bin\Configuration\idsrv3test.pfx", AppDomain.CurrentDomain.BaseDirectory), "idsrv3test");
}
}
EDIT
I have made some progress in redirecting to the Login page. The issue I was having is that the initial call to the web api was being redirected by IdentityServer with a status code of 302. This meant that I could not get access to the Location header which contains the url I want to redirect to. So instead, I added some owin middleware to check the status code, and if a 302, return a 401 instead (I need to also check that the call is Ajax but have not yet implemented this).
app.Use(async (environment, next) =>
{
await next();
if (environment.Response.StatusCode == 302)
{
environment.Response.StatusCode = 401;
}
});
This then gives me access to the Location header on the client side, and I can redirect, like so:
getPlayerList: function (queryParameters) {
var deferred = $q.defer();
$http.post('api/pitchingstats/GetFilteredPlayers', {
skip: queryParameters.skip,
take: queryParameters.take,
orderby: queryParameters.orderby,
sortdirection: queryParameters.sortdirection,
filter: queryParameters.filter
}).success(function (data, status, headers, config) {
if (status === 401) {
window.location = headers().location;
}
deferred.resolve(data);
}).error(function (data, status, headers, config) {
deferred.reject(status);
});
I know it's a hack, but it's the only way I can make my approach work currently. By all means feel free to suggest other methods.