We have developed a set of Web APIs (REST) which are protected by an Authorization server. The Authorization server has issued the client id and client secret. These can be used to obtain an access token. A valid token can be used on subsequent calls to resource servers (REST APIs).
I want to write an web based (Asp.net MVC 5) client that will consume the APIs. Is there a nuget package I can download that will help me to implement the client OAuth2 flow? Can anyone direct me to a good example on client implementation of OAuth2 flow (written in asp.net MVC)?
Update
I was able to get access token using the code block below, but what I want is a "client credentials" oauth 2 flow where I don't have to enter login and passwords. The code I have now is:
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType("ClientCookie");
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
AuthenticationType = "ClientCookie",
CookieName = CookieAuthenticationDefaults.CookiePrefix + "ClientCookie",
ExpireTimeSpan = TimeSpan.FromMinutes(5)
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
AuthenticationType = OpenIdConnectAuthenticationDefaults.AuthenticationType,
SignInAsAuthenticationType = app.GetDefaultSignInAsAuthenticationType(),
ClientId = ConfigurationManager.AppSettings["AuthServer:ClientId"],
ClientSecret = ConfigurationManager.AppSettings["AuthServer:ClientSecret"],
RedirectUri = ConfigurationManager.AppSettings["AuthServer:RedirectUrl"],
Configuration = new OpenIdConnectConfiguration
{
AuthorizationEndpoint = "https://identityserver.com/oauth2/authorize",
TokenEndpoint = "https://identityserver.com/oauth2/token"
},
//ResponseType = "client_credentials", // Doesn't work
ResponseType = "token",
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = notification =>
{
if (string.Equals(notification.ProtocolMessage.Error, "access_denied", StringComparison.Ordinal))
{
notification.HandleResponse();
notification.Response.Redirect("/");
}
return Task.FromResult<object>(null);
},
AuthorizationCodeReceived = async notification =>
{
using (var client = new HttpClient())
{
//var configuration = await notification.Options.ConfigurationManager.GetConfigurationAsync(notification.Request.CallCancelled);
String tokenEndPoint = "https://identityserver.com/oauth2/token";
//var request = new HttpRequestMessage(HttpMethod.Post, configuration.TokenEndpoint);
var request = new HttpRequestMessage(HttpMethod.Post, tokenEndPoint);
request.Content = new FormUrlEncodedContent(new Dictionary<string, string> {
{ OpenIdConnectParameterNames.ClientId, notification.Options.ClientId },
{ OpenIdConnectParameterNames.ClientSecret, notification.Options.ClientSecret },
{ OpenIdConnectParameterNames.Code, notification.ProtocolMessage.Code },
{ OpenIdConnectParameterNames.GrantType, "authorization_code" },
{ OpenIdConnectParameterNames.RedirectUri, notification.Options.RedirectUri }
});
var response = await client.SendAsync(request, notification.Request.CallCancelled);
response.EnsureSuccessStatusCode();
var payload = JObject.Parse(await response.Content.ReadAsStringAsync());
// Add the access token to the returned ClaimsIdentity to make it easier to retrieve.
notification.AuthenticationTicket.Identity.AddClaim(new Claim(
type: OpenIdConnectParameterNames.AccessToken,
value: payload.Value<string>(OpenIdConnectParameterNames.AccessToken)));
}
}
}
});
}
}
To support the client credentials grant type, your best option is probably to directly use HttpClient:
var request = new HttpRequestMessage(HttpMethod.Post, "http://server.com/token");
request.Content = new FormUrlEncodedContent(new Dictionary<string, string> {
{ "client_id", "your client_id" },
{ "client_secret", "your client_secret" },
{ "grant_type", "client_credentials" }
});
var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
var payload = JObject.Parse(await response.Content.ReadAsStringAsync());
var token = payload.Value<string>("access_token");
For interactive flows (like the authorization code flow), there are two better approaches:
If your authorization server supports OpenID Connect (which is based on OAuth2), you can simply use the OpenID Connect middleware for OWIN/Katana 3 developed by Microsoft: https://www.nuget.org/packages/Microsoft.Owin.Security.OpenIdConnect/
If OpenID Connect is not supported by your authorization server, one option is to create your own OAuth2 client middleware. You can take a look at the last part of this SO answer for more information: Registering Web API 2 external logins from multiple API clients with OWIN Identity
Related
I can't get the simple Web Api authorized request via JWT in NET 7 minimal API from a MAUI client app (see below).
Therefore I thought of a workaround solution which looks like this:
from the client side I also send the user data (user and password) for each request:
UserAuthentication user = new UserAuthentication();
user.UserName = "user1";
user.Email = "user1#email.com";
user.Password = "321";
HttpClient httpClient = new HttpClient();
var response = await
httpClient.PostAsJsonAsync("https://api.mywebsite.com/api/data", user);
in the minimal api, I then check the user details at the endpoint and then send the data back if the login matches.
app.MapPost("/api/data",
[AllowAnonymous] (User user) =>.
{
if (user.UserName == "user1" && user.Password == "321")
{
return Results.Ok(list_data);
}
return Results.Unauthorized();
});
Two remarks:
in the release version both user and password should be encrypted
of course the user data comes from the DB, I hard-coded the user here for testing.
Can anyone confirm that I can implement an authorized request like this (i.e. without JWT)? Or have I missed something important here with my reasoning? The important thing is that it is at least as secure as with JWT.
Here again examples that do not work in MAUI Client (are already in other posts of mine). I'm starting to think there's a problem here at Microsoft, but don't know where best to report the problem (did it at ASP-NET-CORE, got rejected https://github.com/dotnet/AspNetCore.Docs/issues/27929 ).
minimal API (NET 7):
app.MapGet("/secret2", [Authorize] () => $"Hello You. This is a secret!!!");
MAUI CLient:
HttpClient httpClient = new HttpClient();
var requestMessage = new HttpRequestMessage
{
Method = HttpMethod.Get,
RequestUri = new Uri(#"https://api.mysite.com/secret2")
};
requestMessage.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
var response = await httpClient.SendAsync(requestMessage);
Although the same JWT works via Postman, here in MAUI I get an Unauthorized message 401 when requesting. I already posted details about this (also network protocol), see also github link.
Thanks
EDIT
According to Tiny Wang, the client code works. I then assume that there is an error in the JWT generation (wondering how the generated token then worked via Postman).
Here is the completely code from Web Api (for the generation and request of JWT, as well as endpoint for a request via JWT).).
using Microsoft.AspNetCore.Authorization;
using Microsoft.IdentityModel.Tokens;
using System.Security.Claims;
using System.Text;
using System.IdentityModel.Tokens.Jwt;
using Microsoft.OpenApi.Models;
using Microsoft.AspNetCore.Authentication.JwtBearer;
namespace WebApplication1
{
public class User
{
public string UserName { get; set; } = "";
public string Email { get; set; } = "";
public string Password { get; set; } = "";
public string AddInfo { get; set; } = "";
}
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization();
builder.Services.AddAuthentication().AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("superSecretKey#345"))
};
});
builder.Services.AddEndpointsApiExplorer();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.MapGet("/api/test", [AllowAnonymous] () => "Hello you!");
app.MapGet("/secret2", [Authorize] () => $"Hello You. This is a secret!!!");
app.MapPost("/security/createToken",
[AllowAnonymous] (User user) =>
{
if (user.UserName == "user" && user.Password == "123")
{
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(JwtRegisteredClaimNames.Iat, DateTime.UtcNow.ToString()),
new Claim(JwtRegisteredClaimNames.GivenName, user.UserName),
new Claim(JwtRegisteredClaimNames.Email, "user#test.com"),
new Claim(ClaimTypes.Role, "Administrator"),
new Claim("Role1", "Administrator"),
new Claim("Role2", "Standard"),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("superSecretKey#345"));
var signinCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256);
var tokeOptions = new JwtSecurityToken(
issuer: "https://api.mysite.com:64591",
audience: "https://api.mysite.com:64591",
claims: claims,
expires: DateTime.Now.AddMinutes(50),
signingCredentials: signinCredentials
);
var tokenString = new JwtSecurityTokenHandler().WriteToken(tokeOptions);
return Results.Ok(tokenString);
}
return Results.Unauthorized();
});
app.UseHttpsRedirection();
app.Run();
}
}
}
The cause of the problem was tricky, because even the many tutorials and posts in the form as they are given in Internet will not work. But if you copy generated token out (e.g. from debug mode) and use it in Postman, then everything will work nicely and this is something that confuses you a lot. Fortunately, there are still people who have incredible mind and can detect such inconsistencies. I wouldn't have seen this in 1000 years either :)
See: https://learn.microsoft.com/en-us/answers/questions/1133200/401-unauthorized-consuming-web-api-with-jwt-authen.html
How does one authenticate as a user without any direct user interaction?
Otherwise i found a workaround with client credential flow in this example : https://github.com/microsoftgraph/console-csharp-snippets-sample
but if i try to implement this code in an c# Asp.net mav applcition or a windows forms application i cant get an application token. If i debug the app it stuck at waiting for token but it doesn't throw an error (virus protection already deactivated). Does anyone have a idea for the main problem or my workaround?
This is the Code for my workaround trying to get a token, but stuck on daemonClient.AcquireTokenForClientAsync.
public async Task<Users> GetUser(string Username)
{
MSALCache appTokenCache = new MSALCache(clientId);
ClientCredential clientdummy = new ClientCredential(clientSecret);
ConfidentialClientApplication daemonClient = new ConfidentialClientApplication(clientId, string.Format(AuthorityFormat, tenantId), redirectUri,
clientdummy, null, null);
authenticate(daemonClient).Wait();
string token = authResult.AccessToken;
client = GetAuthenticatedClientForApp(token);
IGraphServiceUsersCollectionPage users = client.Users.Request().GetAsync().Result;
}
private async Task<AuthenticationResult> authenticate(ConfidentialClientApplication daemonClient)
{
authResult = await daemonClient.AcquireTokenForClientAsync(new[] { MSGraphScope });
return authResult;
}
Found Workaround Solution: getting a Token via REST API. Here I can get an User token or an Client token to access to graph api:
var client = new RestClient("https://login.microsoftonline.com/" + domainname);
var request = new RestRequest("/oauth2/token", Method.POST); request.AddBody("grant_type", "client_credentials");
request.AddParameter("client_id", clientId);
request.AddParameter("client_secret", clientSecret);
request.AddParameter("Resource", "https://graph.microsoft.com");
request.AddParameter("scope", "[scopes]");
IRestResponse response = client.Execute(request);
//contains the token
var content = response.Content;
According to your description, I assume you need a solution to authenticate a user without any interaction.
We can get an Access Token by some background services or daemons.
For more details, we can refer to this document.
Base on my test, we can try the following steps:
First, we should get administrator consent:
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = authority,
RedirectUri = redirectUri,
PostLogoutRedirectUri = redirectUri,
Scope = "openid profile",
ResponseType = "id_token",
TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = false, NameClaimType = "name" },
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = this.OnAuthenticationFailedAsync,
SecurityTokenValidated = this.OnSecurityTokenValidatedAsync
}
});
ConfidentialClientApplication daemonClient = new ConfidentialClientApplication(Startup.clientId, string.Format(AuthorityFormat, tenantId), Startup.redirectUri,
new ClientCredential(Startup.clientSecret), null, appTokenCache.GetMsalCacheInstance());
AuthenticationResult authResult = await daemonClient.AcquireTokenForClientAsync(new[] { MSGraphScope });
Then, we can use this access token to using the Graph APIs.
For more details, we can review to v2.0 daemon sample on GitHub.
I am using OWIN to connect up to O365 via the Microsoft Graph API in an ASP.NET MVC app. Everything is setup in Startup.Auth.cs including the Redirect Uri value which currently comes from the web.config. Authentication is working correctly.
As I am using wildcards in the App Registration, the redirect uri can be a variety of values and te user is able authenticate to O365 from any number of pages in the app. Once authenticated I'd like them to be brought back to the page they were just on but because the redirect uri is already set, they are brought back to that page.
How can I modify the redirect uri, elsewhere in my code, after the OWIN identity context has been created?
Below is a snippet of the startup code.
public partial class Startup
{
public void ConfigureAuth(IAppBuilder app)
{
private static string redirectUri = ConfigurationManager.AppSettings["ida:RedirectUri"];
....
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = appId,
Authority = "https://login.microsoftonline.com/organizations/v2.0",
PostLogoutRedirectUri = redirectUri,
RedirectUri = redirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = async (context) =>
{
Dictionary<string, string> data = new Dictionary<string, string>();
data.Add("client_id", appId);
data.Add("client_secret", appSecret);
data.Add("code", code);
data.Add("grant_type", "authorization_code");
data.Add("redirect_uri", redirectUri);
...
I had a similar situation. I tied into RedirectToIdentityProvider, to modify the RedirectUri before sending the request to the identity provider. Something like the following
Notifications = new OpenIdConnectAuthenticationNotifications()
{
RedirectToIdentityProvider = async (context) =>
{
context.ProtocolMessage.RedirectUri = "Whatever_You_Want_Here";
}
}
I wanted to dynamically determine the redirect_uri of the application and came up with this solution with the help of above answers and comments:
Notifications = new OpenIdConnectAuthenticationNotifications()
{
RedirectToIdentityProvider = (o) =>
{
o.ProtocolMessage.RedirectUri = DetermineRedirectUri(o.Request);
return Task.CompletedTask;
},
AuthorizationCodeReceived = (o) =>
{
o.TokenEndpointRequest.RedirectUri = DetermineRedirectUri(o.Request);
return Task.CompletedTask;
}
}
And the helper method:
private string DetermineRedirectUri(IOwinRequest request)
{
return request.Scheme + System.Uri.SchemeDelimiter + request.Host + request.PathBase;
}
For anyone coming here and wondering where they should put this, it still goes into your statup.auth.cs (or similar) file. Where you need to put it is here:
public void ConfigureAzureAuth(IAppBuilder app)
{
ServicePointManager.Expect100Continue = true;
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls
| SecurityProtocolType.Tls11
| SecurityProtocolType.Tls12
| SecurityProtocolType.Ssl3;
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
// Sets the ClientId, authority, RedirectUri as obtained from web.config
Caption = "Your site",
AuthenticationType = "Your site",
ClientId = clientId,
Authority = authority,
//MetadataAddress = "https://your auth url/.well-known/openid-configuration",
RedirectUri = redirectUri,
// PostLogoutRedirectUri is the page that users will be redirected to after sign-out. In this case, it is using the home page
PostLogoutRedirectUri = "/",
Scope = OpenIdConnectScope.OpenIdProfile,
// ResponseType is set to request the code id_token - which contains basic information about the signed-in user
ResponseType = OpenIdConnectResponseType.CodeIdToken,
// OpenIdConnectAuthenticationNotifications configures OWIN to send notification of failed authentications to OnAuthenticationFailed method
Notifications = new OpenIdConnectAuthenticationNotifications
{
//These are like call backs, so they fire when the event happens.
AuthenticationFailed = OnAuthenticationFailed,
//We can tap into this notification to get the IOwinRequest object
RedirectToIdentityProvider = (o) =>
{
//here is where we set the redirect Uri based on the request we received.
o.ProtocolMessage.RedirectUri = DetermineRedirectUri(o.Request);
return Task.CompletedTask;
},
AuthorizationCodeReceived = (o) =>
{
o.TokenEndpointRequest.RedirectUri = DetermineRedirectUri(o.Request);
return Task.CompletedTask;
}
},
});
}
You will also need the additional method that #brz included.
private string DetermineRedirectUri(IOwinRequest request)
{
return request.Scheme + System.Uri.SchemeDelimiter + request.Host + request.PathBase + "/Account/ExternalLoginCallback/";
}
I have an ASP.NET Core MVC application allowing anonymous users. This app is calling an ASP.NET Web API that is protected by Identity Server 4. I have created a client in Identity Server describing the MVC app (client) and given it access to the api scope like this:
new Client
{
ClientId = "my-mvc-client-app",
AllowedGrantTypes = GrantTypes.ClientCredentials,
RequireConsent = false,
ClientSecrets = new List<Secret> { new Secret("this-is-my-secret".Sha256()) },
AllowedScopes = new List<string>
{
StandardScopes.OpenId.Name,
StandardScopes.Profile.Name,
StandardScopes.OfflineAccess.Name,
"my-protected-api"
},
RedirectUris = new List<string>
{
"http://localhost:5009/signin-oidc",
}
}
In my MVC app, I'm using TokenClient to get a token that I can use when making requests to the protected API like this:
var disco = await DiscoveryClient.GetAsync("http://localhost:5010");
var tokenClient = new TokenClient(disco.TokenEndpoint, clientId, clientSecret);
var tokenResponse = await tokenClient.RequestClientCredentialsAsync("hrmts-test-candidate-api-scope");
This works fine, but I'm requesting new tokens from Identity Server on every request, which is probably not a good idea.
What is the best practice for handling the tokens? How can I persist them on the client (the MVC app) and how can I handle refresh tokens to make sure the client gets a new token when necessary?
You need to wrap that client in a managed service of some kind (as a singleton) so that you can use it anywhere you need. We have a token component that we use for server to server communication that follows this flow:
public class ServerTokenComponent
{
private TokenResponse Token { get; set; }
private DateTime ExpiryTime { get; set; }
public async Task<TokenResponse> GetToken()
{
//use token if it exists and is still fresh
if (Token != null && ExpiryTime > DateTime.UtcNow)
{
return Token;
}
//else get a new token
var client = new TokenClient("myidpauthority.com","theclientId","thesecret")
var scopes = "for bar baz";
var tokenResponse = await client.RequestClientCredentialsAsync(scopes);
if (tokenResponse.IsError || tokenResponse.IsHttpError)
{
throw new SecurityTokenException("Could not retrieve token.");
}
//set Token to the new token and set the expiry time to the new expiry time
Token = tokenResponse;
ExpiryTime = DateTime.UtcNow.AddSeconds(Token.ExpiresIn);
//return fresh token
return Token;
}
}
In other words - you need to cache that token somehow. When you request the token, you get an ExpiresIn in the response - this will tell you how long the token will be valid.
Another option is to wait until the API returns a 401 - and then request a new token.
Refresh tokens are not used with client credentials flow.
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.