How to get OAuth2 OpenId token from Identity Server 4 for client that uses OWIN - asp.net-mvc

I am using IdentityServer4 to get my OpenId tokens for my web app.
But my web app uses Owin for security. I cannot find any example samples of where this is done.
So this code works;
In my client;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OpenIdConnect;
using Owin;
using SIR.API.Caller.Helpers;
namespace SIR.API.Caller
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = Settings.SignInAsAuthenticationType // "Cookies";
});
app.UseOpenIdConnectAuthentication(openIdConnectOptions: new OpenIdConnectAuthenticationOptions
{
AuthenticationType = "oidc",
Authority = Settings.AuthorityUrl1, //ID Server, "https://localhost:44314/"; https://localhost:44307/
ClientId = Settings.ClientId, // "SIR"
Scope = Settings.Scope, // "openid profile";
ResponseType = Settings.ResponseType, // "id_token code";
SignInAsAuthenticationType = Settings.SignInAsAuthenticationType,
//--------------------------------------// "Cookies";
RedirectUri = Settings.RedirectUri, // URL of website, http://localhost:50000/signin-oidc;
//RedirectUri = Settings.RedirectUri1, // URL of website, http://localhost:53200/signin-oidc;
RequireHttpsMetadata = Settings.RequireHttpsMetadata,
//--------------------------------------// true
ClientSecret = "secret"
});
app.Use(async (ctx, next) =>
{
var message = ctx.Authentication.User.Identity.IsAuthenticated
? $"User: {ctx.Authentication.User.Identity.Name}"
: "User Not Authenticated";
await next();
});
}
}
}
In my ID 4 server the startup is;
using System.Security.Cryptography.X509Certificates;
using IdentityServer4;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using Mulalley.IdentityServer4.Helpers;
using QuickstartIdentityServer;
namespace Mulalley.IdentityServer4
{
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
// configure identity server with in-memory stores, keys, clients and scopes
services.AddIdentityServer()
//.AddDeveloperSigningCredential()
.AddSigningCredential(new X509Certificate2(Settings.CertPath, Settings.Password))
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients())
.AddTestUsers(Config.GetUsers());
services.AddAuthentication()
.AddGoogle("Google", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
// register your IdentityServer with Google at https://console.developers.google.com
// enable the Google+ API
// set the redirect URI to http://localhost:port/signin-google
options.ClientId = "copy client ID from Google here";
options.ClientSecret = "copy client secret from Google here";
})
.AddOpenIdConnect("oidc", "OpenID Connect", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.SignOutScheme = IdentityServerConstants.SignoutScheme;
options.Authority = "https://demo.identityserver.io/";
options.ClientId = "implicit";
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
};
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseIdentityServer();
app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
}
}
}
and the Config.cs is;
using IdentityServer4;
using IdentityServer4.Models;
using IdentityServer4.Test;
using System.Collections.Generic;
using System.Security.Claims;
namespace QuickstartIdentityServer
{
public class Config
{
// scopes define the resources in your system
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
};
}
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource("SIR", "Service Inspection Report")
};
}
// clients want to access resources (aka scopes)
public static IEnumerable<Client> GetClients()
{
var baseUri = "http://localhost:53200/";
// client credentials client
return new List<Client>
{
new Client
{
ClientId = "client",
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes = { "SIR" },
AlwaysIncludeUserClaimsInIdToken = true
},
// resource owner password grant client
new Client
{
ClientId = "ro.client",
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes = { "SIR" },
AlwaysIncludeUserClaimsInIdToken = true
},
// OpenID Connect hybrid flow and client credentials client (MVC)
new Client
{
ClientId = "SIR",
ClientName = "SIR",
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
ClientSecrets =
{
new Secret("secret".Sha256())
},
RedirectUris = { baseUri + "signin-oidc" },
PostLogoutRedirectUris = { baseUri + "signout-callback-oidc" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"SIR"
},
AllowOfflineAccess = true,
AlwaysIncludeUserClaimsInIdToken = true
}
};
}
public static List<TestUser> GetUsers()
{
return new List<TestUser>
{
new TestUser
{
SubjectId = "1",
Username = "alice",
Password = "password",
Claims = new List<Claim>
{
new Claim("name", "Alice"),
new Claim("website", "https://alice.com")
}
},
new TestUser
{
SubjectId = "2",
Username = "bob",
Password = "password",
Claims = new List<Claim>
{
new Claim("name", "Bob"),
new Claim("website", "https://bob.com")
}
}
};
}
}
}
And the question is how do I get the token in C# so I can have a look at it? And how do I set the GetClaimsFromUserInfoEndpoint = true which is not available in OWIN?
Are there any code samples I can look at with OWIN access to ID4?
EDIT: This code appears to be what I need: https://identitymodel.readthedocs.io/en/latest/client/token.html
However I put the code in and I get an access token which when I put it in jwt.io I get a message "Invalid Signature".
private static async Task<TokenResponse> GetClientCredentialsTokenResponse()
{
var client = new HttpClient();
var tokenRequest = new ClientCredentialsTokenRequest
{
Address = Settings.AuthorityUrl1 + "connect/token",
ClientId = Settings.ClientId,
ClientSecret = "secret",
Scope = "SIR"
};
return await client.RequestClientCredentialsTokenAsync(tokenRequest);
}
private static async Task<TokenResponse> GetTokenResponse()
{
var client = new HttpClient();
var tokenRequest = new TokenRequest
{
Address = Settings.AuthorityUrl1 + "connect/token",
GrantType = GrantType.ClientCredentials,
ClientId = Settings.ClientId,
ClientSecret = "secret",
Parameters =
{
{"scope", "SIR"}
}
};
return await client.RequestTokenAsync(tokenRequest);
}
private static void ThrowResponseException(TokenResponse response)
{
const string message = "Problem accessing the UserInfo endpoint";
if (response.Exception == null)
{
throw new Exception($"{message}: {response.Error}. {response.ErrorDescription}.");
}
throw new Exception(message, response.Exception);
}
}
}

Related

Issues getting an api with swagger to authenticate with IdentityServer4 using .net5.0

I am currently learning how microservices work for an application i am building for my portfolio and for a small community of people that want this specific application. I have followed a tutorial online and successfully got IdentityServer4 to authenticate an MVC Client, however, I am trying to get swagger to work alongside the API's especially if they require authentication. Each time I try to authorize swagger with IdentityServer4, I am getting invalid_scope error each time I try authenticate. I have been debugging this problem for many hours an am unable to figure out the issue. I have also used the Microsoft eShopOnContainers as an example but still no luck. Any help would be greatly appreciated. Ill try keep the code examples short, please request any code not shown and ill do my best to respond asap. Thank you.
Identiy.API project startup.cs:
public class Startup {
public void ConfigureServices(IServiceCollection services)
{
services.AddIdentityServer()
.AddInMemoryClients(Config.GetClients())
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryApiScopes(Config.GetApiScopes())
.AddTestUsers(Config.GetTestUsers())
.AddDeveloperSigningCredential(); // #note - demo purposes only. need X509Certificate2 for production)
services.AddControllersWithViews();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseStaticFiles();
app.UseIdentityServer();
app.UseAuthorization();
app.UseEndpoints(endpoints => endpoints.MapDefaultControllerRoute());
}
}
Config.cs (i removed the test users to keep the code shorter, since it is not relevant).
#note -I created a WeatherSwaggerUI client specifically for use with swagger since this was apart of eShopOnContainers example project provided by microsoft:
public static class Config
{
public static List<TestUser> GetTestUsers()
{
return new List<TestUser>(); // removed test users for this post
}
public static IEnumerable<Client> GetClients()
{
// #note - clients can be defined in appsettings.json
return new List<Client>
{
// m2m client credentials flow client
new Client
{
ClientId = "m2m.client",
ClientName = "Client Credentials Client",
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets = { new Secret("SuperSecretPassword".ToSha256())},
AllowedScopes = { "weatherapi.read", "weatherapi.write" }
},
// interactive client
new Client
{
ClientId = "interactive",
ClientSecrets = {new Secret("SuperSecretPassword".Sha256())},
AllowedGrantTypes = GrantTypes.Code,
RedirectUris = {"https://localhost:5444/signin-oidc"},
FrontChannelLogoutUri = "https://localhost:5444/signout-oidc",
PostLogoutRedirectUris = {"https://localhost:5444/signout-callback-oidc"},
AllowOfflineAccess = true,
AllowedScopes = {"openid", "profile", "weatherapi.read"},
RequirePkce = true,
RequireConsent = false,
AllowPlainTextPkce = false
},
new Client
{
ClientId = "weatherswaggerui",
ClientName = "Weather Swagger UI",
AllowedGrantTypes = GrantTypes.Implicit,
AllowAccessTokensViaBrowser = true,
RedirectUris = {"https://localhost:5445/swagger/oauth2-redirect.html"},
PostLogoutRedirectUris = { "https://localhost:5445/swagger/" },
AllowedScopes = { "weatherswaggerui.read", "weatherswaggerui.write" },
}
};
}
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource("weatherapi", "Weather Service")
{
Scopes = new List<string> { "weatherapi.read", "weatherapi.write" },
ApiSecrets = new List<Secret> { new Secret("ScopeSecret".Sha256()) },
UserClaims = new List<string> { "role" }
},
new ApiResource("weatherswaggerui", "Weather Swagger UI")
{
Scopes = new List<string> { "weatherswaggerui.read", "weatherswaggerui.write" }
}
};
}
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResource
{
Name = "role",
UserClaims = new List<string> { "role" }
},
};
}
public static IEnumerable<ApiScope> GetApiScopes()
{
return new List<ApiScope>
{
// weather API specific scopes
new ApiScope("weatherapi.read"),
new ApiScope("weatherapi.write"),
// SWAGGER TEST weather API specific scopes
new ApiScope("weatherswaggerui.read"),
new ApiScope("weatherswaggerui.write")
};
}
}
Next project is the just the standard weather api when creating a web api project with vs2019
WeatherAPI Project startup.cs (note i created extension methods as found in eShopOnContainers as i liked that flow):
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddCustomAuthentication(Configuration)
.AddSwagger(Configuration);
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger()
.UseSwaggerUI(options =>
{
options.SwaggerEndpoint("/swagger/v1/swagger.json", "Weather.API V1");
options.OAuthClientId("weatherswaggerui");
options.OAuthAppName("Weather Swagger UI");
});
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
public static class CustomExtensionMethods
{
public static IServiceCollection AddSwagger(this IServiceCollection services, IConfiguration configuration)
{
services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo
{
Title = "Find Scrims - Weather HTTP API Test",
Version = "v1",
Description = "Randomly generates weather data for API testing"
});
options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme
{
Type = SecuritySchemeType.OAuth2,
Flows = new OpenApiOAuthFlows()
{
Implicit = new OpenApiOAuthFlow()
{
AuthorizationUrl = new Uri($"{ configuration.GetValue<string>("IdentityUrl")}/connect/authorize"),
TokenUrl = new Uri($"{ configuration.GetValue<string>("IdentityUrl")}/connect/token"),
Scopes = new Dictionary<string, string>()
{
{ "weatherswaggerui", "Weather Swagger UI" }
},
}
}
});
options.OperationFilter<AuthorizeCheckOperationFilter>();
});
return services;
}
public static IServiceCollection AddCustomAuthentication(this IServiceCollection services, IConfiguration configuration)
{
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("sub");
var identityUrl = configuration.GetValue<string>("IdentityUrl");
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.Authority = identityUrl;
options.RequireHttpsMetadata = false;
options.Audience = "weatherapi";
});
return services;
}
}
Lastly is the AuthorizeCheckOperationFilter.cs
public class AuthorizeCheckOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var hasAuthorize = context.MethodInfo.DeclaringType.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any() ||
context.MethodInfo.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any();
if (!hasAuthorize) return;
operation.Responses.TryAdd("401", new OpenApiResponse { Description = "Unauthorized" });
operation.Responses.TryAdd("403", new OpenApiResponse { Description = "Forbidden" });
var oAuthScheme = new OpenApiSecurityScheme
{
Reference = new OpenApiReference {
Type = ReferenceType.SecurityScheme,
Id = "oauth2"
}
};
operation.Security = new List<OpenApiSecurityRequirement>
{
new OpenApiSecurityRequirement
{
[oAuthScheme ] = new [] { "weatherswaggerui" }
}
};
}
}
again, any help, or recommendations on a guide would be greatly appreciated as google has not provided me with any results to fixing this issue. Im quite new to IdentityServer4 and am assuming its a small issue due with clients and ApiResources and ApiScopes. Thank you.
The swagger client needs to access the api and to do so it requires api scopes. What you have for swagger scopes are not doing this. Change the scopes for swagger client ‘weatherswaggerui’ to include the api scopes like this:
AllowedScopes = {"weatherapi.read"}

IdentityServer4 Implicit GrantType throws Unauthorized_Client Error

When I run my project and try to login to get an access token, I get an Unauthorized_Client error on the browser or when testing with Postman. I'm fairly new to IdentityServer. This is my Configuration:
Config.cs
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email(),
// custom identity resource with some consolidated claims
new IdentityResource("custom.profile", new[] { JwtClaimTypes.Name, JwtClaimTypes.Email, "location" })
};
}
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
//new ApiResource("NuB.hAPI", "Hospital API")
new ApiResource
{
Name = "NuB.HospitalSearch",
ApiSecrets = { new Secret("F621F470-9731-4A25-80EF-67A6F7C5F4B8".Sha256()) },
UserClaims =
{
JwtClaimTypes.Name,
JwtClaimTypes.Email,
JwtClaimTypes.PhoneNumber,
JwtClaimTypes.Gender,
"NuB.HospitalSearch"
},
Scopes =
{
new Scope
{
Name = "NuB.HospitalSearch",
DisplayName = "Full access to Hospital Search App"
},
new Scope
{
Name = "openid",
DisplayName = "Read only access to Hospital Search App"
}
}
}
};
}
// clients want to access resources (aka scopes)
public static IEnumerable<Client> GetClients()
{
// client credentials client
return new List<Client>
{
new Client
{
ClientId = "mvcWeb",
ClientName = "MVC Web Client",
AllowedGrantTypes = GrantTypes.Implicit,
AccessTokenType = AccessTokenType.Jwt,
RequireConsent = true,
ClientSecrets =
{
new Secret("secret".Sha256())
},
RedirectUris = { "http://localhost:5002/signin-oidc" },
PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"NuB.HospitalSearch"
},
AllowOfflineAccess = true
}
};
}
I am using ASP.NET Identity with EntityFrameworkCore with IdentityServer4 to do this. It may by a simple problem but kindly point me to the right direction.
Unauthorized_Client means that your client is trying to authencate to the Ids with a client that does not exist. You apear to have created a client called mvcWeb
That means that your client will need to use a client id of mvcWeb your client code should probably look something like this. Note options.ClientId = "mvcWeb";
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = settingsSetup.Authority;
options.RequireHttpsMetadata = false;
options.ClientId = "mvcWeb";
options.ClientSecret = "secret";
options.ResponseType = oidcConstants.ResponseTypes.CodeIdTokenToken;
options.Scope.Add("openid");
options.Scope.Add("profile");
});

Social Signout not working in conjunction with OpenIdConnect

I created an MVC Web Application with OpenIdConnect authentication (for Azure Authentication) and Authentication providers for Google, Facebook and Microsoft Account.
The Configuration in StartupAuth looks like this:
public void ConfigureAuth(IAppBuilder app)
{
if (Config.TaskboardUserSource == Config.DirectoryService.AzureAD)
{
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
ExpireTimeSpan = new TimeSpan(6, 0, 0),
SlidingExpiration = true,
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Home/Index"),
Provider = new CookieAuthenticationProvider
{
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: TimeSpan.FromMinutes(30),
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
}
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5));
app.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie);
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = Config.ClientId,
Authority = string.Format("{0}common", Config.AadInstance),
UseTokenLifetime = false,
TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters
{
ValidateIssuer = false,
},
Notifications = new OpenIdConnectAuthenticationNotifications()
{
SecurityTokenValidated = (context) =>
{
return Task.FromResult(0);
},
AuthorizationCodeReceived = (context) =>
{
var code = context.Code;
ClientCredential credential = new ClientCredential(Config.ClientId, Config.AppKey);
string tenantID = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;
string signedInUserID = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value;
AuthenticationContext authContext = new AuthenticationContext(string.Format("{0}{1}", Config.AadInstance, tenantID), new ADALTokenCache(signedInUserID));
AuthenticationResult result = authContext.AcquireTokenByAuthorizationCodeAsync(
code,
new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)),
credential,
Config.GraphResourceID).Result;
return Task.FromResult(0);
},
RedirectToIdentityProvider = (context) =>
{
// This ensures that the address used for sign in and sign out is picked up dynamically from the request
// this allows you to deploy your app (to Azure Web Sites, for example)without having to change settings
// Remember that the base URL of the address used here must be provisioned in Azure AD beforehand.
string appBaseUrl = context.Request.Scheme + "://" + context.Request.Host + context.Request.PathBase;
context.ProtocolMessage.RedirectUri = appBaseUrl + "/";
context.ProtocolMessage.PostLogoutRedirectUri = appBaseUrl;
return Task.FromResult(0);
},
AuthenticationFailed = (context) =>
{
context.OwinContext.Response.Redirect("/Home/Index");
context.HandleResponse(); // Suppress the exception
return Task.FromResult(0);
}
}
});
var facebookAuthenticationOptions = new FacebookAuthenticationOptions()
{
AppId = Config.FBAppId,
AppSecret = Config.FBAppSecret,
UserInformationEndpoint = Config.FBUserInformationEndpoint
};
facebookAuthenticationOptions.Scope.Add("email");
app.UseFacebookAuthentication(facebookAuthenticationOptions);
app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions()
{
ClientId = Config.GoogleClientId,
ClientSecret = Config.GoogleClientSecret
});
var microsoftOptions = new MicrosoftAccountAuthenticationOptions()
{
ClientId = Config.MSAppId,
ClientSecret = Config.MSAppSecret,
};
microsoftOptions.Scope.Add("wl.basic");
microsoftOptions.Scope.Add("wl.emails");
app.UseMicrosoftAccountAuthentication(microsoftOptions);
}
}
All authentication options work fine.
When I want to signout, the only signout working is OpenIdConnect Signout.
For all other authentication providers, the cookie is still available and just by clicking the "Logon" Button the secured pages are shown without asking for a password.
My Signout looks like this:
public void SignOut()
{
string callbackUrl = Url.Action("SignOutCallback", "Account", routeValues: null, protocol: Request.Url.Scheme);
HttpContext.GetOwinContext().Authentication.SignOut(
new AuthenticationProperties { RedirectUri = callbackUrl },
HttpContext.GetOwinContext()
.Authentication.GetAuthenticationTypes()
.Select(o => o.AuthenticationType).ToArray());
HttpContext.GetOwinContext().Authentication.SignOut(
new AuthenticationProperties { RedirectUri = callbackUrl },
CookieAuthenticationDefaults.AuthenticationType);
}
How can I make sure the user is signed out and gets redirected to the start page?
After I inserted a switch case statement in my signout code to do the signout for every logonprovider it finally works. Here is my code:
public async Task<ActionResult> SignOut()
{
var currentUser = await UserService.CurrentUser();
if (currentUser != null)
{
var redirectUrl = Request.GetBaseUrl();
var loginProviders = new string[] {
"Google",
"TwoFactorRememberBrowser",
"TwoFactorCookie",
"ExternalCookie",
"ApplicationCookie"
};
switch (currentUser.LoginProvider)
{
case LogonProvider.FacebookProviderKey:
{
loginProviders = new string[] {
"Facebook",
"TwoFactorRememberBrowser",
"TwoFactorCookie",
"ExternalCookie",
"ApplicationCookie" };
break;
}
case LogonProvider.GoogleProviderKey:
{
loginProviders = new string[] {
"Google",
"TwoFactorRememberBrowser",
"TwoFactorCookie",
"ExternalCookie",
"ApplicationCookie" };
//return new RedirectResult($"https://www.google.com/accounts/Logout");
break;
}
case LogonProvider.MicrosoftProviderKey:
{
loginProviders = new string[] {
"Microsoft",
"TwoFactorRememberBrowser",
"TwoFactorCookie",
"ExternalCookie",
"ApplicationCookie" };
break;
}
default:
{
loginProviders = new string[] {
"Office365",
"TwoFactorRememberBrowser",
"TwoFactorCookie",
"ExternalCookie",
"ApplicationCookie" };
break;
}
}
HttpContext.GetOwinContext().Authentication.SignOut(new AuthenticationProperties { RedirectUri = redirectUrl }, loginProviders);
}
return RedirectToAction("Index", "Home");
}

Integrating IdentityServer4 With WebAPI

I am trying to integrate IdentityServer4 with ASP.NET MVC WebAPI.I want to achieve role based authorization. I have the following projects running.
IdentityServer4 [a separate project]
WebApi
Javascript Application [using Extjs]
I have implemented the ResourceOwnerPassword flow and what I am trying to do is,
Make a post request to AccountController of the WebApi containing username and password
Inside the AccountController call the IdentityServer token endpoint for an access token and return the access token to the client (javascript app)
Make a request to the WebApi containing the access token.
For the above part I am success full, here is the code
POSTMAN call for Login
AccountController
[ActionName("Login")]
[AllowAnonymous]
[HttpPost]
public async Task<BaseModel> Login(LoginModel model)
{
model.RememberMe = false;
var status = await _security.Login(model.Email, model.Password, model.RememberMe);
if (status.Status == LoginStatus.Succeded)
{
return new BaseModel { success = true, message = "login", data = status.Data };
}
}
SecurityService
public async Task<LoginResponse> Login(string userName, string password, bool persistCookie = false)
{
// discover endpoints from metadata
var disco = await DiscoveryClient.GetAsync("http://localhost:5000");
// request token
var tokenClient = new TokenClient(disco.TokenEndpoint, "ro.client", "secret");
var tokenResponse = await tokenClient.RequestResourceOwnerPasswordAsync(userName, password, "api1");
if (tokenResponse.IsError)
{
return new LoginResponse { Status = LoginStatus.Failed, Message = tokenResponse.Error };
}
return new LoginResponse { Status = LoginStatus.Succeded, Data = tokenResponse.Json };
}
Security the API
I have two more actions inside the AccountController (just for testing) namely:
values() [returns success and requires no authentication]
SecureValues [returns success and requires authentication]
[HttpGet]
public BaseModel values()
{
return new BaseModel
{
success = true
};
}
[Authorize]
[HttpGet]
public BaseModel SecureValues()
{
return new BaseModel
{
success = true
};
}
calling "Values" action returns the success which is quite obvious, calling the "SecureValues" gives following
Which means that the user is not Authenticated.
My IdentityServer4 configuration is as follows:
public class Config
{
// scopes define the resources in your system
public static IEnumerable<Scope> GetScopes()
{
return new List<Scope>
{
StandardScopes.OpenId,
StandardScopes.Profile,
new Scope
{
Name = "api1",
Description = "My API",
DisplayName = "API Access",
Type = ScopeType.Resource,
IncludeAllClaimsForUser = true,
Claims = new List<ScopeClaim>
{
new ScopeClaim(ClaimTypes.Name),
new ScopeClaim(ClaimTypes.Role)
}
},
new Scope
{
Enabled = true,
Name = "role",
DisplayName = "Role(s)",
Description = "roles of user",
Type = ScopeType.Identity,
Claims = new List<ScopeClaim>
{
new ScopeClaim(ClaimTypes.Role,false)
}
},
StandardScopes.AllClaims
};
}
// clients want to access resources (aka scopes)
public static IEnumerable<Client> GetClients()
{
// client credentials client
return new List<Client>
{
new Client
{
ClientId = "client",
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes = { "api1" }
},
// resource owner password grant client
new Client
{
ClientId = "ro.client",
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes = { "api1" }
},
// OpenID Connect implicit flow client (MVC)
new Client
{
ClientId = "mvc",
ClientName = "MVC Client",
AllowedGrantTypes = GrantTypes.Implicit,
//flow
RedirectUris = { "http://localhost:5002/signin-oidc" },
PostLogoutRedirectUris = { "http://localhost:5002" },
AllowedScopes =
{
StandardScopes.OpenId.Name,
StandardScopes.Profile.Name,
"role"
}
},
//for hybrid flow
new Client
{
ClientId = "mvchybrid",
ClientName ="mvc hybrid client",
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
ClientSecrets =
{
new Secret("secret".Sha256())
},
RedirectUris = {"http://localhost:5003/signin-oidc"},
PostLogoutRedirectUris = {"http://localhost:5003"},
AllowedScopes =
{
StandardScopes.OpenId.Name,
StandardScopes.Profile.Name,
StandardScopes.OfflineAccess.Name,
"api1"
}
},
new Client
{
ClientId = "js",
ClientName = "javascript client",
AllowedGrantTypes = GrantTypes.Implicit,
AllowAccessTokensViaBrowser= true,
RedirectUris = {"http://localhost:5004/callback.html"},
PostLogoutRedirectUris = {"http://localhost:5004/index.html"},
AllowedCorsOrigins = {"http://localhost:5004"},
AllowedScopes =
{
StandardScopes.OpenId.Name,
StandardScopes.Profile.Name,
"api1",
"role",
StandardScopes.AllClaims.Name
}
},
//aspnet identity client
new Client
{
ClientId = "mvcIdentity",
ClientName = "Mvc Identity Client",
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
RequireConsent = false,
ClientSecrets =
{
new Secret("secret".Sha256())
},
RedirectUris = {"http://localhost:5005/signin-oidc"},
PostLogoutRedirectUris = {"http://localhost:5005"},
AllowedScopes =
{
StandardScopes.OpenId.Name,
StandardScopes.Profile.Name,
StandardScopes.OfflineAccess.Name,
"api1"
}
}
};
}
public static List<InMemoryUser> GetUsers()
{
return new List<InMemoryUser>
{
new InMemoryUser
{
Subject = "1",
Username = "alice#yahoo.com",
Password = "password",
Claims = new List<Claim>
{
new Claim("name", "Alice"),
new Claim("website", "https://alice.com"),
new Claim(ClaimTypes.Role,"FreeUser")
}
},
new InMemoryUser
{
Subject = "2",
Username = "bob#yahoo.com",
Password = "password",
Claims = new List<Claim>
{
new Claim("name", "Bob"),
new Claim("website", "https://bob.com"),
new Claim(ClaimTypes.Role,"PaidUser")
}
}
};
}
}
WebApi configuration
public void ConfigureAuth(IAppBuilder app)
{
app.UseIdentityServerBearerTokenAuthentication(new IdentityServer3.AccessTokenValidation.IdentityServerBearerTokenAuthenticationOptions
{
Authority = "localhost:5000",
RequiredScopes = new[] { "api1" },
ClientId = "ro.client",
ClientSecret = "secret",
PreserveAccessToken = true
});
}
It looks like your Authority in your UseIdentityServerBearerTokenAuthentication middleware is wrong. I think it should be "http://localhost:5000".
Also enabling logging (with trace) to console you can see why your authorization was challenged sometimes.

Connecting with Javascript to Web API protected by IdentityServer3

I have a Asp.NET MVC / WebAPI project with an embedded IdentityServer3.
I want both MVC and WebAPI to be protected by the IdentityServer. So I have used Authorize attribute on both MVC controllers and API controllers.
When surfing to my test page (which is protected) I get redirected to the IdentityServer login page. I enter my username and password and get authenticated and redirected back.
On the page I have a button that triggers a GET from javascript, with my access token in the authorization header, to my protected API. But here it fails with a 401 Unauthorized.
I get the access token to javascript by rendering it to the page with Razor.
I have 1 client in the IdentityServer set to hybrid flow. MVC uses cookies, while the API uses bearer tokens.
On my API HttpConfiguration I have set SuppressDefaultHostAuthentication. If I remove that line everything works, but then it uses cookies for the API which I don't want.
I use only HTTP and RequireSsl=false for now to avoid potential certificate problems.
I have tried for days to get this to work but I'm getting nowhere.
I don't even know how to debug this to get to cause of the 401.
By now I recognize just about every page that google suggests when I search for help.
Any ideas what it could be or how to debug this?
Here is my Startup.cs
public class Startup
{
public void Configuration(IAppBuilder app)
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Trace()
.CreateLogger();
// MVC client
string authBaseAddress = "http://localhost:50319/identity";
string tokenEndpoint = authBaseAddress + "/connect/token";
string userInfoEndpoint = authBaseAddress + "/connect/userinfo";
string redirectUri = "http://localhost:50319/";
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies"
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = "hybrid_clients",
Authority = authBaseAddress,
RedirectUri = redirectUri,
ResponseType = "code id_token token",
Scope = "openid profile roles sampleApi offline_access",
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
},
SignInAsAuthenticationType = "Cookies",
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = async n =>
{
// use the code to get the access and refresh token
var tokenClient = new TokenClient(
tokenEndpoint,
"hybrid_clients",
"secret");
var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(n.Code, n.RedirectUri);
if (tokenResponse.IsError)
{
throw new Exception(tokenResponse.Error);
}
// use the access token to retrieve claims from userinfo
var userInfoClient = new UserInfoClient(
new Uri(userInfoEndpoint),
tokenResponse.AccessToken);
var userInfoResponse = await userInfoClient.GetAsync();
// create new identity
var id = new ClaimsIdentity(n.AuthenticationTicket.Identity.AuthenticationType);
id.AddClaims(userInfoResponse.GetClaimsIdentity().Claims);
id.AddClaim(new Claim("access_token", tokenResponse.AccessToken));
id.AddClaim(new Claim("expires_at", DateTime.Now.AddSeconds(tokenResponse.ExpiresIn).ToLocalTime().ToString()));
id.AddClaim(new Claim("refresh_token", tokenResponse.RefreshToken));
id.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
id.AddClaim(new Claim("sid", n.AuthenticationTicket.Identity.FindFirst("sid").Value));
n.AuthenticationTicket = new AuthenticationTicket(
new ClaimsIdentity(id.Claims, n.AuthenticationTicket.Identity.AuthenticationType, "name", "role"),
n.AuthenticationTicket.Properties);
},
// Attach the id_token for the logout roundtrip to IdentityServer
RedirectToIdentityProvider = n =>
{
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.LogoutRequest)
{
var idTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token");
if (idTokenHint != null)
{
n.ProtocolMessage.IdTokenHint = idTokenHint.Value;
}
}
return Task.FromResult(0);
}
}
});
AntiForgeryConfig.UniqueClaimTypeIdentifier = Constants.ClaimTypes.Subject;
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
// web api
app.Map("/api", a =>
{
var config = new HttpConfiguration();
a.UseCors(CorsOptions.AllowAll);
a.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
//AuthenticationMode = AuthenticationMode.Active,
Authority = authBaseAddress,
RequiredScopes = new[] { "sampleApi" },
DelayLoadMetadata = true
});
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
config.MapHttpAttributeRoutes();
a.UseWebApi(config);
});
// Identity server
app.Map("/identity", idsrvApp =>
{
idsrvApp.UseCors(CorsOptions.AllowAll);
idsrvApp.UseIdentityServer(new IdentityServerOptions
{
SiteName = "Embedded IdentityServer",
SigningCertificate = LoadCertificate(),
Factory = new IdentityServerServiceFactory()
.UseInMemoryUsers(Users.Get())
.UseInMemoryClients(Clients.Get())
.UseInMemoryScopes(Scopes.Get()),
AuthenticationOptions = new IdentityServer3.Core.Configuration.AuthenticationOptions()
{
EnablePostSignOutAutoRedirect = true // Automatically redirects back to the client on signout
},
RequireSsl = false,
});
});
}
X509Certificate2 LoadCertificate()
{
return new X509Certificate2(
string.Format(#"{0}\bin\idsrv3test.pfx", AppDomain.CurrentDomain.BaseDirectory), "idsrv3test");
}
}
My client
new Client
{
Enabled = true,
ClientName = "Hybrid Clients",
ClientId = "hybrid_clients",
Flow = Flows.Hybrid,
//AllowAccessTokensViaBrowser = false,
RedirectUris = new List<string>
{
"http://localhost:50319/"
},
PostLogoutRedirectUris = new List<string>
{
"http://localhost:50319/"
},
AllowedScopes = new List<string>
{
"openid",
"profile",
"email",
"roles",
"address",
"all_claims",
"sampleApi",
"offline_access"
},
ClientSecrets = new List<Secret>
{
new Secret("secret".Sha256())
},
AccessTokenType = AccessTokenType.Reference,
LogoutSessionRequired = true
},
My scopes
public static class Scopes
{
public static IEnumerable<Scope> Get()
{
var scopes = new List<Scope>
{
StandardScopes.OpenId,
StandardScopes.Profile,
StandardScopes.Email,
StandardScopes.Address,
StandardScopes.OfflineAccess,
StandardScopes.RolesAlwaysInclude,
StandardScopes.AllClaims,
new Scope
{
Enabled = true,
Name = "roles",
Type = ScopeType.Identity,
Claims = new List<ScopeClaim>
{
new ScopeClaim("role")
}
},
new Scope
{
Enabled = true,
DisplayName = "Sample API",
Name = "sampleApi",
Description = "Access to a sample API",
Type = ScopeType.Resource
}
};
return scopes;
}
}
My API
[Authorize]
public class SecuredApiController : ApiController
{
public IHttpActionResult Get()
{
var user = User as ClaimsPrincipal;
var claims = from c in user.Claims
select new
{
type = c.Type,
value = c.Value
};
return Json(claims);
}
}
Part of my Razor view
<button data-bind="click:callApi">Call API</button>
<span data-bind="text:apiResult"></span>
<script>
$(function() {
ko.myViewModel = new ClientAppViewModel('#ViewData["access_token"]');
ko.applyBindings(ko.myViewModel);
});
</script>
My JavaScript (KnockoutJS) that calls SecuredApi
function ClientAppViewModel(accessToken) {
var self = this;
self.accessToken = accessToken;
self.apiResult = ko.observable('empty');
self.callApi = function () {
console.log('CallApi');
var xhr = new XMLHttpRequest();
xhr.open("GET", "http://localhost:50319/api/SecuredApi");
xhr.onload = function () {
self.apiResult(JSON.stringify(JSON.parse(xhr.response), null, 2));
};
xhr.setRequestHeader("Authorization", "Bearer " + self.accessToken);
xhr.send();
}
}
The Katana logs for the API are what you want. With these you will see why the API is returning a 401.
You can access these using the Microsoft.Owin log source (see Katana Documentation)

Resources