My SPA application (using Aurelia) calls my ASP.NET Core 2 Web API. I need to authenticate users with Google OIDC provider and also secure the Web API with the same method.
Currently I'm able to authenticate user on the client (SPA) side and retrieve id token and access token. With each API call I send the access token in the header.
Now I'm not sure how to handle the server side to validate the token and grant or deny the access to the API. I followed official docs how to add external login providers, but it seem to work only for server-side MVC applications.
Is there any easy way how to do this?
I think for instance IdentityServer4 can support this scenario, but it seems to me too complex for what I need to do. I don't need my own identity/authorization server after all.
Update:
Based on Miroslav Popovic answer, my configuration for ASP.NET Core 2.0 looks like this:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(o =>
{
o.Authority = "https://accounts.google.com";
o.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = "accounts.google.com",
ValidAudience = "xxxxxxxxxxxxx.apps.googleusercontent.com",
ValidateAudience = true,
ValidateIssuer = true
};
});
services.AddMvc();
}
And in Configure() I call app.UseAuthentication().
When using this setup I get failure message No SecurityTokenValidator available for token.
Update 2:
I made it work. The server configuration is correct. The problem was I was sending access_token to the API instead of id_token.
Since you already have the access token, it shouldn't be too hard to use it to add authentication. You would need something along these lines (not tested):
// Inside Startup.cs, ConfigureServices method
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(
options =>
{
var tokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = "accounts.google.com",
ValidateAudience = false
};
options.MetadataAddress = "https://accounts.google.com/.well-known/openid-configuration";
options.TokenValidationParameters = tokenValidationParameters;
});
// Inside Startup.cs, Configure method
app.UseAuthentication(); // Before MVC middleware
app.UseMvc();
// And of course, on your controllers:
[Authorize]
public class MyApiController : Controller
This post from Paul Rowe might help some more, but note that it's written for ASP.NET Core 1.x and authentication APIs changed a bit in 2.0.
There is also a lot of info here on SO, like this question.
Related
I have an asp.net core web api. Say if i want to have two versions of the api such that, version 1 will require a bearer token and version 2 will not require the barer token.
Since the token configuration code resides in the startup.cs file, how do I have two startup.cs files to match my requirement above? i am not even sure if it is legal to have two startup.cs files targeting two different versions of the api because the code to configure versioning of an asp.net core api will also reside in the startup.cs file.
Let me know what options are available to achieve my requirement above.
My current startup.cs file with token authentication enabled look like this..
public void ConfigureServices(IServiceCollection services)
{
services.Configure<AzureADSettings>(Configuration.GetSection("AzureAd"));
var azureADSettings = Configuration.GetSection("AzureAd").Get<AzureADSettings>();
var validAudience = new List<string>
{
azureADSettings.Audience
};
services.AddAuthentication(o =>
{
o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(o =>
{
o.Authority = $"{azureADSettings.Instance}/{azureADSettings.TenantId}/";
o.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = true,
//azureADSettings.Audience
ValidAudiences = new List<string>(validAudience)
};
});
}
The Authorize middleware applies that check if you do not explicitly a controller or action as Anonymous. Maybe what you can do is:
Put the [Anonymous] attribute on top of your controller(s).
Mark your v1 API end-points in your controller(s) as [Authorize].
Leave the v2 API end-points as-is.
This way, the v2 API end-points should work fine with users not having a bearer token but v1 API end-points should expect a valid bearer token.
Requirement is that I have MVC & WebAPI in the project. MVC views will be delivered for initial
rendering like Login, base views of features (ex:- Users/Index, Dashboard/Index etc..) Web APIs will be used for other work within these views via AJAX with JWT.
I am using Asp.Net core Identity for user management related work running on .Net 5.0
I am confused with configuring multiple identity schemes and the proper configuration of authentication/authorization pipeline in conigureservices() & configure() in startup.
To configure multiple auth schemes I referred to https://stackoverflow.com/a/64887479/2058413 since it's done using same versions that I use. So my startup methods are below which is similar to the code in that thread.
public void ConfigureServices(IServiceCollection services)
{
string connectionString = Configuration.GetConnectionString("default");
services.AddDbContext<AppDBContext>(c => c.UseSqlServer(connectionString));
services.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<AppDBContext>();
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(x =>
{
x.LoginPath = "/Account/Login";
x.ExpireTimeSpan = TimeSpan.FromMinutes(10d);
x.AccessDeniedPath = "/Account/Register";
})
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("123456")),
ValidateIssuer = false,
ValidateAudience = false
};
});
services.AddAuthorization(options =>
{
var defaultAuthorizationPolicyBuilder = new AuthorizationPolicyBuilder(CookieAuthenticationDefaults.AuthenticationScheme, JwtBearerDefaults.AuthenticationScheme);
defaultAuthorizationPolicyBuilder = defaultAuthorizationPolicyBuilder.RequireAuthenticatedUser();
options.DefaultPolicy = defaultAuthorizationPolicyBuilder.Build();
});
services.AddControllersWithViews();
}
My App configure method is below
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
Then the test method in controller(where user should get redirected to after authentication) is below
[Authorize(AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)]
public IActionResult Index()
{
return View();
}
To my understanding the order of the pipeline configuration is correct. Now here are the problems I face.
As specified in .AddCookie(option=>) , user doesn't get redirected to login page. However, if I remove the JwtBearerDefaults.AuthenticationScheme from the services.AddAuthorization(…) it gets redirected to login page properly. Why is that?
So I remove JwtBearerDefaults.AuthenticationScheme; which takes me to login and after successful login I can see that HttpContext.User.Identity.IsAuthenticated is set to true. But it doesn't redirect to Home/Index. No errors thrown and in browser console [network tab] it shows a 302 and redirect back to login. Since I have added [Authorize(AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)] to Index method in HomeController, I tried removing the scheme and adding [Authorize] and tried again. Still it gets redirected to login page. This is another thing I am confused about.
So I removed everything related to dual scheme authentication and left the ConfigureService() with below code
string connectionString = Configuration.GetConnectionString("default");
services.AddDbContext<AppDBContext>(c => c.UseSqlServer(connectionString));
services.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<AppDBContext>();
services.AddControllersWithViews();
Now everything works fine (redirection to login if not authenticated and also redirects to /Home/Index after authorization).
I went through below links as well about multi scheme authentication, but I am still confused with this pipeline configuration.
ASP.NET Core WebAPI Cookie + JWT Authentication
https://wildermuth.com/2017/08/19/Two-AuthorizationSchemes-in-ASP-NET-Core-2
https://mitchelsellers.com/blog/article/using-multiple-authentication-authorization-providers-in-asp-net-core
I need help only to this multi-scheme authentication pipeline configuration part.
Ok, after some research the main issue was;
I have mixed up old ways of registering services in StartUp (asp.net core 3.x). So had to clear up all those. Resources that helped me to do that;
Migrate from ASP.NET Core 3.1 to 5.0
ASP.NET Core Middleware
This cleaned up a lot of unnecessary code since in .Net 5 there were some shortcuts that could be used.
Order of service registrations. This may depend on what kind of services you are using, but in my case the order was something like below:
AddIdentity
setup Cookie & JWT authentication c)
My Multitenant stuff
AddCors
AddModules (will be option for some of you. I use it to load plugins dynamically)
Other stuff (However, even in these places the order might matter depending on
what you do)
The other thing was, I had to remove ConfigureApplicationCookie() since AddIdentity seems to be doing that. Also in AddAuthorization() earlier I had code to specify what are the default schemes i wanted to use (Cookie & JWT). But now I had to remove all that. Reason is Identity takes over the cookie scheme and when I specify below, JWTAuth takes over.
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
Generally setting up Startup properly seems to be tricky depending on different services you use. Order will matter
I have one project (Project A) which is a .NET CORE API project using Openiddict with an endpoint of /connect/token to issue JWT tokens using Identity to handle the security etc. This project works great as is.
I have another project (Project B), which is just a very simple project with some HTML that makes requests to the API to get an access token, and get data from the API. This project also works great.
Now the part I cannot wrap my brain around, how do I use Facebook login between these two totally separate projects? I know how to use it if everything is under one roof, and it's really easy, but this scenario has me totally confused since everything is separated. So for starters, who handles the 'ExternalLogin', 'ExternalLoginCallBack' logic (from .NET web template using individual accounts), the API? The HTML project? When connecting with Facebook, what redirect uri should I use (API/HTML project)? Then who should have the below code in their 'Startup.cs' file?
app.UseFacebookAuthentication(new FacebookOptions
{
AppId = "xxxxxxx",
AppSecret = "xxxxxxxxx",
Scope = { "email", "user_friends" },
Fields = { "name", "email" },
SaveTokens = true,
});
And finally if this helps here is how I have Project A currently setup:
STARTUP.CS (API)
public void ConfigureServices function: (API)
// add entity framework using the config connection string
services.AddEntityFrameworkSqlServer()
.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
// add identity
services.AddIdentity<ApplicationUser, ApplicationRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
// add OpenIddict
services.AddOpenIddict<ApplicationUser, ApplicationRole, ApplicationDbContext>()
.DisableHttpsRequirement()
.EnableTokenEndpoint("/connect/token")
.AllowPasswordFlow()
.AllowRefreshTokenFlow()
.UseJsonWebTokens()
.AddEphemeralSigningKey();
services.AddCors();
public void Configure function: (API)
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
RequireHttpsMetadata = false,
Audience = "http://localhost:54418/",
Authority = "http://localhost:54418/"
});
Authorization Controller (API)
public class AuthorizationController : Controller
{
private OpenIddictUserManager<ApplicationUser> _userManager;
public AuthorizationController(OpenIddictUserManager<ApplicationUser> userManager)
{
_userManager = userManager;
}
[HttpPost("~/connect/token")]
[Produces("application/json")]
public async Task<IActionResult> Exchange()
{
var request = HttpContext.GetOpenIdConnectRequest();
if (request.IsPasswordGrantType())
{
var user = await _userManager.FindByNameAsync(request.Username);
if (user == null)
{
return BadRequest(new OpenIdConnectResponse
{
ErrorDescription = "The username or password provided is incorrect"
});
}
var identity = await _userManager.CreateIdentityAsync(user, request.GetScopes());
// Add a custom claim that will be persisted
// in both the access and the identity tokens.
if (user.Avatar != null)
{
identity.AddClaim("user_avatar", user.Avatar,
OpenIdConnectConstants.Destinations.AccessToken,
OpenIdConnectConstants.Destinations.IdentityToken);
}
if (user.InSiteUserName != null)
{
identity.AddClaim("insite_username", user.InSiteUserName,
OpenIdConnectConstants.Destinations.AccessToken,
OpenIdConnectConstants.Destinations.IdentityToken);
}
identity.AddClaim("hasLoggedIn", user.HasLoggedIn.ToString(),
OpenIdConnectConstants.Destinations.AccessToken,
OpenIdConnectConstants.Destinations.IdentityToken);
// Create a new authentication ticket holding the user identity.
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(identity),
new AuthenticationProperties(),
OpenIdConnectServerDefaults.AuthenticationScheme);
ticket.SetResources(request.GetResources());
ticket.SetScopes(request.GetScopes());
return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
}
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.UnsupportedGrantType,
ErrorDescription = "The specified grant type is not supported."
});
}
}
}
I don't know if it's including anything from Project B since it's pretty basic/bare and relies on the API for everything.
I know this is a loaded and complicated question, and I'm sure I'm not presenting it as fluidly as possible so I apologize in advance for that, like I said before, I'm confused. Thank you!
Now the part I cannot wrap my brain around, how do I use Facebook login between these two totally separate projects? I know how to use it if everything is under one roof, and it's really easy, but this scenario has me totally confused since everything is separated. So for starters, who handles the 'ExternalLogin', 'ExternalLoginCallBack' logic (from .NET web template using individual accounts), the API? The HTML project?
In the recommended case (i.e when using an interactive flow like the authorization code flow or the implicit flow), the authorization server project itself is responsible of handling the external authentication dance, using the social providers you've configured in your ASP.NET Core pipeline.
In theory, the final client application (i.e the JS app) doesn't even know that you've decided to use external authentication at the authorization server level, since it's not directly linked to Facebook or Google.
In this case, the redirect_uri configured in the Facebook options must correspond to an endpoint owned by the authorization server application (in your case, it's provided by the Facebook authentication middleware).
If you don't like this approach, there's also a different flow named "assertion grant", that basically reverses how things are handled: the final client app (the JS app in your case) is directly linked to Facebook - so the redirect_uri must correspond to the JS app - and uses OpenIddict's token endpoint to "exchange" Facebook tokens with tokens issued by your own server, that can be used with your own APIs.
For more information about this flow, please read Exchanging a google idToken for local openId token c#.
We have a web app built on Asp.Net core. It doesn't contain any authentication middleware configured in it.
We are hosting on Azure App Service and using the Authentication/Authorization option (EasyAuth) to authenticate against Azure AD.
The authentication works well - we get the requisite headers inserted and we can see the authenticated identity at /.auth/me. But the HttpContext.User property doesn't get populated.
Is this a compatibility issue for Asp.Net core? Or am I doing something wrong?
I've created a custom middleware that populates the User property until this gets solved by the Azure Team.
It reads the headers from the App Service Authentication and create a a user that will be recognized by the [Authorize] and has a claim on name.
// Azure app service will send the x-ms-client-principal-id when authenticated
app.Use(async (context, next) =>
{
// Create a user on current thread from provided header
if (context.Request.Headers.ContainsKey("X-MS-CLIENT-PRINCIPAL-ID"))
{
// Read headers from Azure
var azureAppServicePrincipalIdHeader = context.Request.Headers["X-MS-CLIENT-PRINCIPAL-ID"][0];
var azureAppServicePrincipalNameHeader = context.Request.Headers["X-MS-CLIENT-PRINCIPAL-NAME"][0];
// Create claims id
var claims = new Claim[] {
new System.Security.Claims.Claim("http://schemas.microsoft.com/identity/claims/objectidentifier", azureAppServicePrincipalIdHeader),
new System.Security.Claims.Claim("name", azureAppServicePrincipalNameHeader)
};
// Set user in current context as claims principal
var identity = new GenericIdentity(azureAppServicePrincipalIdHeader);
identity.AddClaims(claims);
// Set current thread user to identity
context.User = new GenericPrincipal(identity, null);
};
await next.Invoke();
});
Yes, this is a compatibility issue. ASP.NET Core does not support flowing identity info from an IIS module (like Easy Auth) to the app code, unfortunately. This means HttpContext.User and similar code won't work like it does with regular ASP.NET.
The workaround for now is to invoke your web app's /.auth/me endpoint from your server code to get the user claims. You can then cache this data as appropriate using the x-ms-client-principal-id request header value as the cache key. The /.auth/me call will need to be properly authenticated in the same way that calls to your web app need to be authenticated (auth cookie or request header token).
I wrote a small basic middleware to do this. It will create an identity based off of the .auth/me endpoint. The identity is created in the authentication pipeline so that [authorize] attributes and policies work with the identity.
You can find it here:
https://github.com/lpunderscore/azureappservice-authentication-middleware
or on nuget:
https://www.nuget.org/packages/AzureAppserviceAuthenticationMiddleware/
Once added, just add this line to your startup:
app.UseAzureAppServiceAuthentication();
The following code decrypts the AAD token from the Azure App Service HTTP header and populates HttpContext.User with the claims. It's rough as you'd want to cache the configuration rather than look it up on every request:
OpenIdConnectConfigurationRetriever r = new OpenIdConnectConfigurationRetriever();
ConfigurationManager<OpenIdConnectConfiguration> configManager = new ConfigurationManager<OpenIdConnectConfiguration>(options.Endpoint, r);
OpenIdConnectConfiguration config = await configManager.GetConfigurationAsync();
var tokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKeys = config.SigningKeys.ToList(),
ValidateIssuer = true,
ValidIssuer = config.Issuer,
ValidateAudience = true,
ValidAudience = options.Audience,
ValidateLifetime = true,
ClockSkew = new TimeSpan(0, 0, 10)
};
JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
ClaimsPrincipal principal = null;
SecurityToken validToken = null;
string token = context.Request.Headers["X-MS-TOKEN-AAD-ID-TOKEN"];
if (!String.IsNullOrWhiteSpace(token))
{
principal = handler.ValidateToken(token, tokenValidationParameters, out validToken);
var validJwt = validToken as JwtSecurityToken;
if (validJwt == null) { throw new ArgumentException("Invalid JWT"); }
if (principal != null)
{
context.User.AddIdentities(principal.Identities);
}
}
It only works for Azure AD. To support other ID providers (Facebook, Twitter, etc) you'd have to detect the relevant headers and figure out how to parse each provider's token. However, it should just be variations on the above theme.
You can give this library a try. I faced a similar problem and created this to simplify the use.
https://github.com/dasiths/NEasyAuthMiddleware
Azure App Service Authentication (EasyAuth) middleware for ASP.NET
CORE with fully customizable components with support for local
debugging
It hydrates the HttpContext.User by registering a custom authentication handler. To make things easier when running locally, it even has the ability to use a json file to load mocked claims.
What I want is a method of JWT Generation and JWT Consumption in ASP.NET Core.
No OAuth2 flow, I do have the IdentityServerv3 working with OAuth2 but it is just overkill for a single app accessing an API when I own both sides.
The main source of difficulty I am having is finding out the equivalent of Microsoft.Owin.Security.Jwt in ASP.NET Core. Nothing in this list https://www.myget.org/gallery/aspnetvnext seems to relate. Or is that package actually to stay relevant in with ASP.NET Core?
If you're looking for a (simple) way to generate your own JWT tokens, you should directly use the JwtSecurityTokenHandler. You can find it in the System.IdentityModel.Tokens package on the MyGet repository you mentioned (but the version is a bit old now) or directly on the Azure AD repository, in the System.IdentityModel.Tokens.Jwt package: https://www.myget.org/gallery/azureadwebstacknightly
Of course, using a standard protocol to issue and retrieve your JWT tokens is more than recommended and OAuth2 and OpenID Connect are probably the best candidates for that.
Note that IdentityServer is not the only server that works on ASP.NET 5. I'm personally working on an advanced fork of the OAuth2 authorization server middleware that comes with Katana 3 and that offers a different approach: https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Server
app.UseOAuthBearerAuthentication(new JwtBearerOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
Audience = "http://localhost:54540/",
Authority = "http://localhost:54540/"
});
app.UseOpenIdConnectServer(options =>
{
options.Provider = new AuthorizationProvider();
});
To learn more about this project, I'd recommend reading http://kevinchalet.com/2016/07/13/creating-your-own-openid-connect-server-with-asos-introduction/.
Feel free to ping me on https://jabbr.net/#/rooms/AspNetCore if you need more information.
I've started using OpenIddict and I think it is exactly what you need.
This is essentially all the configuration I needed:
ConfigureServices:
services.AddIdentity<ApplicationUser, ApplicationRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders()
.AddOpenIddictCore<Application>(config => config.UseEntityFramework());
Configure
app.UseOpenIddictCore(builder =>
{
// tell openiddict you're wanting to use jwt tokens
builder.Options.UseJwtTokens();
// NOTE: for dev consumption only! for live, this is not encouraged!
builder.Options.AllowInsecureHttp = true;
builder.Options.ApplicationCanDisplayErrors = true;
});
// use jwt bearer authentication
app.UseJwtBearerAuthentication(options =>
{
options.AutomaticAuthenticate = true;
options.AutomaticChallenge = true;
options.RequireHttpsMetadata = false;
options.Audience = "http://localhost:58292/";
options.Authority = "http://localhost:58292/";
});
There are one or two other minor things, such as your DbContext needs to derive from OpenIddictContext<ApplicationUser, Application, ApplicationRole, string>.
You can see a full length explanation (including links to the github repo) on this blog post of mine:
http://capesean.co.za/blog/asp-net-5-jwt-tokens/