I am working on a sample application for OpenIddict using AngularJs. I was told that you shouldnt use clientside frameworks like Satellizer, as this isnt recommended, but instead allow the server to deal with logging in server side (locally and using external login providers), and return the access token.
Well i have a demo angularJs application and uses server side login logic and calls back to the angular app, but my problem is, how do i get the access token for the current user?
here is my startup.cs file, so you can see my configuration so far
public void ConfigureServices(IServiceCollection services) {
var configuration = new ConfigurationBuilder()
.AddJsonFile("config.json")
.AddEnvironmentVariables()
.Build();
services.AddMvc();
services.AddEntityFramework()
.AddSqlServer()
.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(configuration["Data:DefaultConnection:ConnectionString"]));
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders()
.AddOpenIddict();
services.AddTransient<IEmailSender, AuthMessageSender>();
services.AddTransient<ISmsSender, AuthMessageSender>();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
env.EnvironmentName = "Development";
var factory = app.ApplicationServices.GetRequiredService<ILoggerFactory>();
factory.AddConsole();
factory.AddDebug();
app.UseDeveloperExceptionPage();
app.UseIISPlatformHandler(options => {
options.FlowWindowsAuthentication = false;
});
app.UseOverrideHeaders(options => {
options.ForwardedOptions = ForwardedHeaders.All;
});
app.UseStaticFiles();
// Add a middleware used to validate access
// tokens and protect the API endpoints.
app.UseOAuthValidation();
// comment this out and you get an error saying
// InvalidOperationException: No authentication handler is configured to handle the scheme: Microsoft.AspNet.Identity.External
app.UseIdentity();
// TOO: Remove
app.UseGoogleAuthentication(options => {
options.ClientId = "XXX";
options.ClientSecret = "XXX";
});
app.UseTwitterAuthentication(options => {
options.ConsumerKey = "XXX";
options.ConsumerSecret = "XXX";
});
// Note: OpenIddict must be added after
// ASP.NET Identity and the external providers.
app.UseOpenIddict(options =>
{
options.Options.AllowInsecureHttp = true;
options.Options.UseJwtTokens();
});
app.UseMvcWithDefaultRoute();
using (var context = app.ApplicationServices.GetRequiredService<ApplicationDbContext>()) {
context.Database.EnsureCreated();
// Add Mvc.Client to the known applications.
if (!context.Applications.Any()) {
context.Applications.Add(new Application {
Id = "myClient",
DisplayName = "My client application",
RedirectUri = "http://localhost:5000/signin",
LogoutRedirectUri = "http://localhost:5000/",
Secret = Crypto.HashPassword("secret_secret_secret"),
Type = OpenIddictConstants.ApplicationTypes.Confidential
});
context.SaveChanges();
}
}
}
Now my AccountController is basically the same as the normal Account Controller, although once users have logged in (using local and external signin) i use this function and need an accessToken.
private IActionResult RedirectToAngular()
{
// I need the accessToken here
return RedirectToAction(nameof(AccountController.Angular), new { accessToken = token });
}
As you can see from the ExternalLoginCallback method on the AccountController
public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null)
{
var info = await _signInManager.GetExternalLoginInfoAsync();
if (info == null)
{
return RedirectToAction(nameof(Login));
}
// Sign in the user with this external login provider if the user already has a login.
var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false);
if (result.Succeeded)
{
// SHOULDNT THE USER HAVE A LOCAL ACCESS TOKEN NOW??
return RedirectToAngular();
}
if (result.RequiresTwoFactor)
{
return RedirectToAction(nameof(SendCode), new { ReturnUrl = returnUrl });
}
if (result.IsLockedOut)
{
return View("Lockout");
}
else {
// If the user does not have an account, then ask the user to create an account.
ViewData["ReturnUrl"] = returnUrl;
ViewData["LoginProvider"] = info.LoginProvider;
var email = info.ExternalPrincipal.FindFirstValue(ClaimTypes.Email);
return View("ExternalLoginConfirmation", new ExternalLoginConfirmationViewModel { Email = email });
}
}
var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false);
if (result.Succeeded)
{
// SHOULDNT THE USER HAVE A LOCAL ACCESS TOKEN NOW??
return RedirectToAngular();
}
That's not how it's supposed to work. Here's what happens in the classical flow:
The OAuth2/OpenID Connect client application (in your case, your Satellizer JS app) redirects the user agent to the authorization endpoint (/connect/authorize by default in OpenIddict) with all the mandatory parameters: client_id, redirect_uri (mandatory in OpenID Connect), response_type and nonce when using the implicit flow (i.e response_type=id_token token). Satellizer should do that for you, assuming you've correctly registered your authorization server (1).
If the user is not already logged in, the authorization endpoint redirects the user to the login endpoint (in OpenIddict, it's done for you by an internal controller). At this point, your AccountController.Login action is invoked and the user is displayed a login form.
When the user is logged in (after a registration process and/or an external authentication association), he/she MUST be redirected back to the authorization endpoint: you can't redirect the user agent to your Angular app at this stage. Undo the changes made to ExternalLoginCallback and it should work.
Then, the user is displayed a consent form indicating he/she's about to allow your JS app to access his personal data on his/her behalf. When the user submits the consent form, the request is handled by OpenIddict, an access token is generated and the user agent is redirected back to the JS client app, with the token appended to the URI fragment.
[1]: according to the Satellizer documentation, it should be something like that:
$authProvider.oauth2({
name: 'openiddict',
clientId: 'myClient',
redirectUri: window.location.origin + '/done',
authorizationEndpoint: window.location.origin + '/connect/authorize',
responseType: 'id_token token',
scope: ['openid'],
requiredUrlParams: ['scope', 'nonce'],
nonce: function() { return "TODO: implement appropriate nonce generation and validation"; },
popupOptions: { width: 1028, height: 529 }
});
Related
I have setup Asp.Net Identity in my application, and would like to have the following setup:
Have a selfmade username&password authentication as IdentityConstants.ApplicationScheme
Have external providers (e.g. Google, Facebook) which authenticate as IdentityConstants.ExternalScheme
My social login looks like this:
[HttpGet]
[Route("ExternalLogin")]
public IActionResult ExternalLogin([FromQuery] string provider, [FromQuery] bool rememberMe)
{
var redirectUrl = Url.Action("SocialLoginCallback", "SocialLogin", new
{
rememberMe
});
var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
return Challenge(properties, provider);
}
[HttpGet]
[Route("SocialLoginCallback")]
public async Task<IActionResult> SocialLoginCallback([FromQuery] bool rememberMe)
{
// Grab the external login information from the http context
var loginInfo = await _signInManager.GetExternalLoginInfoAsync();
if (loginInfo is null)
{
return Problem();
}
var signinResult = await _signInManager.ExternalLoginSignInAsync(loginInfo.LoginProvider, loginInfo.ProviderKey, rememberMe, true);
if (signinResult.Succeeded)
{
return Ok();
}
return Ok();
}
My startup google auth looks like:
authBuilder.AddGoogle(options =>
{
options.ClientId = Configuration["GoogleClientId"];
options.ClientSecret = Configuration["GoogleClientSecret"];
options.SignInScheme = IdentityConstants.ExternalScheme;
});
However, I noticed that the login is still executed with IdentityConstants.ApplicationScheme.
After examining why, the problem seems to stem from the var signinResult = await _signInManager.ExternalLoginSignInAsync(loginInfo.LoginProvider, loginInfo.ProviderKey, rememberMe, true); call. Internally this runs into a call to await Context.SignInAsync(IdentityConstants.ApplicationScheme, userPrincipal, authenticationProperties ?? new AuthenticationProperties());, which has the ApplicationScheme hardcoded.
How can I get this to work as I would like to have?
I would like to distinguish both logins in a middleware, and as of now, the only way to do this would be to check the respective claims, but I'd rather just to a simple check which auth scheme the login is using. Is this possible?
If you want to set the default authentication method ,You can set DefaultScheme in ConfigureServices/Startup.cs.
public void ConfigureServices(IServiceCollection services)
{
//......
services.AddAuthentication(option =>
{
option.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(option =>
{
option.LoginPath = "/Identity/Account/Login";
})
.AddGoogle(option => {
option.ClientId = "xxx";
option.ClientSecret = "xxx";
});
}
I set the login page as the default Identity login page, when I access the action whitch has [Authorize] attribute , the login page has two ways to log in.
If the app have multiple instances of an authentication handler , You can use [Authorize(AuthenticationSchemes = xxxx)] to Select the scheme with the Authorize attribute.
You can read this document to learn more details about Authorize with a specific scheme in ASP.NET Core
I have a problem enabling Facebook auth in my ASP.NET Core web app. I'm using ASP.NET Core Authentication but not Identity. The auth is configured in Startup like this:
services
.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
.AddFacebook(options =>
{
options.ClientId = "clientid";
options.ClientSecret = "secret";
options.CallbackPath = "/signinfacebookcallback";
});
As shown in the code, I want to use cookie auth, but also allow people to sign in with Facebook. Once they have been successfully signed in, I want to set the auth cookie.
To show the challenge, I have the following action:
[HttpGet]
[Route("signinfacebook")]
public ActionResult SignInFacebook()
{
return Challenge(FacebookDefaults.AuthenticationScheme);
}
This redirects the user to the Facebook login screen. Once they sign in, the user is redirected to the URL specified in config:
[HttpGet]
[Route("signinfacebookcallback")]
public async Task<ActionResult> SignInFacebookCallback()
{
var result = await HttpContext.AuthenticateAsync();
if (!result.Succeeded) return Redirect("/login/");
...
}
When I debug the code, result.Succeeded returns false and the AuthenticationResult object doesn't contain more information on why Succeeded is false.
I verified that the app id and secret are correct.
What could be the issue here?
The CallbackPath in the OpenID Connect middleware is internal path that are used for the authentication flow of the OpenID Connect protocol. After Identity provider redirect user to that url in your application , middeware will handle token valiation ,token decode,exchange token and finally fill the user principle , and that process is fired before your controller gets involved .
Since CallbackPath is internal and will handled by OpenID Connect middleware automatically , you don't need to care about , make sure the callback is registered in facebook's allowed redirect url and let middleware handle the callback .If you want to redirect user to specific route/page after authentication , put the url to AuthenticationProperties :
if (!User.Identity.IsAuthenticated)
{
return Challenge(new AuthenticationProperties() { RedirectUri = "/home/Privacy" } ,FacebookDefaults.AuthenticationScheme);
}
And you should remove the callback path route (signinfacebookcallback) in your application .
UPDATE
If you want to access database and manage local user , you can use built-in events in middleware, for AddFacebook middleware , you can use OnTicketReceived to add access database , manage users and add claims to user's princple :
.AddFacebook(options =>
{
options.ClientId = "xxxxxxxxxxxxx";
options.ClientSecret = "xxxxxxxxxxxxxxxxxxxx";
options.CallbackPath = "/signinfacebookcallback";
options.Events = new OAuthEvents
{
OnTicketReceived = ctx =>
{
//query the database to get the role
var db = ctx.HttpContext.RequestServices.GetRequiredService<YourDbContext>();
// add claims
var claims = new List<Claim>
{
new Claim(ClaimTypes.Role, "Admin")
};
var appIdentity = new ClaimsIdentity(claims);
ctx.Principal.AddIdentity(appIdentity);
return Task.CompletedTask;
},
};
});
I have the following useful load in a token generated with JWT
{
"sub": "flamelsoft#gmail.com",
"jti": "0bca1034-f3ce-4f72-bd91-65c1a61924c4",
"http://schemas.microsoft.com/ws/2008/06/identity/claims/role": "Administrator",
"exp": 1509480891,
"iss": "http://localhost:40528",
"aud": "http://localhost:40528"
}
with this code
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<DBContextSCM>(options =>
options.UseMySql(Configuration.GetConnectionString("DefaultConnection"), b =>
b.MigrationsAssembly("FlamelsoftSCM")));
services.AddIdentity<User, Role>()
.AddEntityFrameworkStores<DBContextSCM>()
.AddDefaultTokenProviders();
services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
services.AddAuthentication()
.AddJwtBearer(cfg =>
{
cfg.RequireHttpsMetadata = false;
cfg.SaveToken = true;
cfg.TokenValidationParameters = new TokenValidationParameters()
{
ValidIssuer = Configuration["Tokens:Issuer"],
ValidAudience = Configuration["Tokens:Issuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Tokens:Key"]))
};
});
services.AddMvc();
}
AccountController.cs
[HttpPost]
[Authorize(Roles="Administrator")]
public async Task<IActionResult> Register([FromBody]RegisterModel model)
{
try
{
var user = new User { UserName = model.Email, Email = model.Email };
var result = await _userManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
var role = await _roleManager.FindByIdAsync(model.Role);
result = await _userManager.AddToRoleAsync(user, role.Name);
if (result.Succeeded)
return View(model);
}
return BadRequest($"Error: Could not create user");
}
catch (Exception ex)
{
return BadRequest($"Error: {ex.Message}");
}
}
user.service.ts
export class UserService {
constructor(private http: Http, private config: AppConfig, private currentUser: User) { }
create(user: User) {
return this.http.post(this.config.apiUrl + 'Account/Register', user, this.jwt());
}
private jwt() {
const userJson = localStorage.getItem('currentUser');
this.currentUser = userJson !== null ? JSON.parse(userJson) : new User();
if (this.currentUser && this.currentUser.token) {
let headers = new Headers({ 'Authorization': 'Bearer ' + this.currentUser.token });
return new RequestOptions({ headers: headers });
}
}}
The problem is that the validation of the role does not work, the request arrives at the controller and returns a code 200 in the header, but never enters the class.
When I remove the [Authorize (Roles = "Administrator")] it enters correctly my code.
Is there something badly defined? Or what would be the alternative to define an authorization through roles.
TL;DR
As mentioned in the comments of the original question, changing:
[HttpPost]
[Authorize(Roles = "Administrator")]
public async Task<IActionResult> Register([FromBody]RegisterModel model)
{
// Code
}
to
[HttpPost]
[Authorize(AuthenticationSchemes = "Bearer", Roles = "Administrator")]
public async Task<IActionResult> Register([FromBody]RegisterModel model)
{
// Code
}
resolved the issue.
Bearer is the default authentication scheme name when using JWT bearer authentication in ASP.NET Core.
But why do we need to specify the AuthenticationSchemes property on the [Authorize] attribute?
It's because configuring authentication schemes doesn't mean they will run on each HTTP request. If a specific action is accessible to anonymous users, why bother extract user information from a cookie or a token? MVC is smart about this and will only run authentication handlers when it's needed, that is, during requests that are somehow protected.
In our case, MVC discovers the [Authorize] attribute, hence knows it has to run authentication and authorization to determine if the request is authorized or not. The trick lies in the fact that it will only run the authentication schemes handlers which have been specified. Here, we had none, so no authentication was performed, which meant authorization failed since the request was considered anonymous.
Adding the authentication scheme to the attribute instructed MVC to run that handler, which extracted user information from the token in the HTTP request, which lead to the Administrator role being discovered, and the request was allowed.
As a side note, there's another way to achieve this, without resorting to using the AuthenticationSchemes property of the [Authorize] attribute.
Imagine that your application only has one authentication scheme configured, it would be a pain to have to specify that AuthenticationSchemes property on every [Authorize] attribute.
With ASP.NET Core, you can configure a default authentication scheme. Doing so implies that the associated handler will be run for each HTTP request, regardless of whether the resource is protected or not.
Setting this up is done in two parts:
public class Startup
{
public void ConfiguresServices(IServiceCollection services)
{
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme /* this sets the default authentication scheme */)
.AddJwtBearer(options =>
{
// Configure options here
});
}
public void Configure(IApplicationBuilder app)
{
// This inserts the middleware that will execute the
// default authentication scheme handler on every request
app.UseAuthentication();
app.UseMvc();
}
}
Doing this means that by the time MVC evaluates whether the request is authorized or not, authentication will have taken place already, so not specifying any value for the AuthenticationSchemes property of the [Authorize] attribute won't be a problem.
The authorization part of the process will still run and check against the authenticated user whether they're part of the Administrator group or not.
I know this question already has an answer, but something important is left out here. You need to make sure you're actually setting the claims for the logged in user. In my case, I'm using JWT Authentication, so this step is very important:
var claims = new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, user.UserName) });
var roles = await _userManager.GetRolesAsync(user);
if (roles.Count > 0)
{
foreach (var role in roles) { claims.AddClaim(new Claim(ClaimTypes.Role, role)); }
}
var token = new JwtSecurityToken(
issuer: _configuration["JWT:Issuer"],
audience: _configuration["JWT:Audience"],
expires: DateTime.UtcNow.AddMinutes(15),
signingCredentials: signingCredentials,
claims: claims.Claims);
I was banging my head trying to figure out why HttpContext.User didn't include what I expected trying to narrow down the [Authroization(Roles="Admin")] issue. Turns out, if you're using JWT Auth you need to remember to set the Claims[] to the identity. Maybe this is done automatically in other dotnet ways, but jwt seems to require you to set that manually.
After I set the claims for the user, the [Authorize(Roles = "Whatever")] worked as expected.
We are already running an ASP.NET MVC web application which is using internal users via token authentication. This is implemented in the standard way the ASP.NET MVC template provides.
Now we have a requirement to extend this authentication model and allow external Azure AD users to sign into the web application for configured tenant. I have figured out everything on the Azure AD side. Thanks to Microsoft's Azure Samples example.
Now both individual account authentication and Azure AD are working well independently. But they're not working together. When I insert both middleware together its giving issue.
Here's my startup_auth.cs file:
public partial class Startup
{
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: TimeSpan.FromMinutes(30),
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
}
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
string ClientId = ConfigurationManager.AppSettings["ida:ClientID"];
string Authority = "https://login.microsoftonline.com/common/";
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = ClientId,
Authority = Authority,
TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters
{
ValidateIssuer = false,
},
Notifications = new OpenIdConnectAuthenticationNotifications()
{
RedirectToIdentityProvider = (context) =>
{
string appBaseUrl = context.Request.Scheme + "://" + context.Request.Host + context.Request.PathBase;
context.ProtocolMessage.RedirectUri = appBaseUrl;
context.ProtocolMessage.PostLogoutRedirectUri = appBaseUrl;
return Task.FromResult(0);
},
SecurityTokenValidated = (context) =>
{
// retriever caller data from the incoming principal
string issuer = context.AuthenticationTicket.Identity.FindFirst("iss").Value;
string UPN = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.Name).Value;
string tenantID = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;
if (
// the caller comes from an admin-consented, recorded issuer
(db.Tenants.FirstOrDefault(a => ((a.IssValue == issuer) && (a.AdminConsented))) == null)
// the caller is recorded in the db of users who went through the individual onboardoing
&& (db.Users.FirstOrDefault(b =>((b.UPN == UPN) && (b.TenantID == tenantID))) == null)
)
// the caller was neither from a trusted issuer or a registered user - throw to block the authentication flow
throw new SecurityTokenValidationException();
return Task.FromResult(0);
},
AuthenticationFailed = (context) =>
{
context.OwinContext.Response.Redirect("/Home/Error?message=" + context.Exception.Message);
context.HandleResponse(); // Suppress the exception
return Task.FromResult(0);
}
}
});
}
}
This configuration works well for local user accounts but doesn't work for AAD. To enable AAD authentication, I need to configure the UseCookieAuthentication part as shown below, which will break my local user account authentication.
app.UseCookieAuthentication(new CookieAuthenticationOptions { });
Basically I need to remove the local users middleware to get AAD work.
What I mean by AAD not working is, I am not able to go to any secured action which is protected by the [Authorize] attribute. It's calling the SecurityTokenValidated event, and I am able to get all AAD claims and able to validate against my custom tenant. But when I redirect to root of my app (which is a secured action) at the end, it throws me back to my custom login page. Seems it's not internally signing in the user and not creating the necessary authentication cookies.
I would appreciate any ideas on what I could be missing here.
Thanks
To support both individual accounts and other account from social data provider, only need to add them using OWIN component.
And to sign-out the users which login from Azure AD, we need to sign-out both the cookie issued from web app and Azure AD. First, I modified the ApplicationUser class to add the custom claim to detect whether the users login from Azure AD or individual accounts like below.
public class ApplicationUser : IdentityUser
{
public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager)
{
// Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
// Add custom user claims here
if((this.Logins as System.Collections.Generic.List<IdentityUserLogin>).Count>0)
userIdentity.AddClaim(new Claim("idp", (this.Logins as System.Collections.Generic.List<IdentityUserLogin>)[0].LoginProvider));
return userIdentity;
}
}
Then we can change the LogOff method to support sign-out from Azure AD to clear the cookies from Azure AD:
// POST: /Account/LogOff
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult LogOff()
{
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie);
var idpClaim = ClaimsPrincipal.Current.Claims.FirstOrDefault(claim => { return claim.Type == "idp"; });
if (idpClaim!=null)
HttpContext.GetOwinContext().Authentication.SignOut(
OpenIdConnectAuthenticationDefaults.AuthenticationType, CookieAuthenticationDefaults.AuthenticationType);
return RedirectToAction("Index", "Home");
}
It sounds like your OpenID Connect auth is not connecting to your Cookie auth. It looks like you need to specify a SignInAsAuthenticationType in your OpenIdConnectAuthenticationOptions that matches the AuthenticationType in your CookieAuthenticationOptions or your ExternalCookie auth type.
still periodically struggling with OpenAuth using OpenIdDict (credentials flow) in ASP.NET Core, I updated to the latest OpenIdDict bits and VS2017 my old sample code you can find at https://github.com/Myrmex/repro-oidang, with a full step-by-step guidance to create an essential startup template. Hope this can be useful to the community to help getting started with simple security scenarios, so any contribution to that simple example code is welcome.
Essentially I followed the credentials flow sample from the OpenIdDict author, and I can get my token back when requesting it like (using Fiddler):
POST http://localhost:50728/connect/token
Content-Type: application/x-www-form-urlencoded
grant_type=password&scope=offline_access profile email roles&resource=http://localhost:4200&username=zeus&password=P4ssw0rd!
Problem is that when I try to use this token, I keep getting a 401, without any other hint: no exception, nothing logged. The request is like:
GET http://localhost:50728/api/values
Content-Type: application/json
Authorization: Bearer ...
Here is my relevant code: first Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
// setup options with DI
// https://docs.asp.net/en/latest/fundamentals/configuration.html
services.AddOptions();
// CORS (note: if using Azure, remember to enable CORS in the portal, too!)
services.AddCors();
// add entity framework and its context(s) using in-memory
// (or use the commented line to use a connection string to a real DB)
services.AddEntityFrameworkSqlServer()
.AddDbContext<ApplicationDbContext>(options =>
{
// options.UseSqlServer(Configuration.GetConnectionString("Authentication")));
options.UseInMemoryDatabase();
// register the entity sets needed by OpenIddict.
// Note: use the generic overload if you need
// to replace the default OpenIddict entities.
options.UseOpenIddict();
});
// register the Identity services
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
// configure Identity to use the same JWT claims as OpenIddict instead
// of the legacy WS-Federation claims it uses by default (ClaimTypes),
// which saves you from doing the mapping in your authorization controller.
services.Configure<IdentityOptions>(options =>
{
options.ClaimsIdentity.UserNameClaimType = OpenIdConnectConstants.Claims.Name;
options.ClaimsIdentity.UserIdClaimType = OpenIdConnectConstants.Claims.Subject;
options.ClaimsIdentity.RoleClaimType = OpenIdConnectConstants.Claims.Role;
});
// register the OpenIddict services
services.AddOpenIddict(options =>
{
// register the Entity Framework stores
options.AddEntityFrameworkCoreStores<ApplicationDbContext>();
// register the ASP.NET Core MVC binder used by OpenIddict.
// Note: if you don't call this method, you won't be able to
// bind OpenIdConnectRequest or OpenIdConnectResponse parameters
// to action methods. Alternatively, you can still use the lower-level
// HttpContext.GetOpenIdConnectRequest() API.
options.AddMvcBinders();
// enable the endpoints
options.EnableTokenEndpoint("/connect/token");
options.EnableLogoutEndpoint("/connect/logout");
// http://openid.net/specs/openid-connect-core-1_0.html#UserInfo
options.EnableUserinfoEndpoint("/connect/userinfo");
// enable the password flow
options.AllowPasswordFlow();
options.AllowRefreshTokenFlow();
// during development, you can disable the HTTPS requirement
options.DisableHttpsRequirement();
// Note: to use JWT access tokens instead of the default
// encrypted format, the following lines are required:
// options.UseJsonWebTokens();
// options.AddEphemeralSigningKey();
});
// add framework services
services.AddMvc()
.AddJsonOptions(options =>
{
options.SerializerSettings.ContractResolver =
new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver();
});
// seed the database with the demo user details
services.AddTransient<IDatabaseInitializer, DatabaseInitializer>();
// swagger
services.AddSwaggerGen();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory,
IDatabaseInitializer databaseInitializer)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
loggerFactory.AddNLog();
// https://learn.microsoft.com/en-us/aspnet/core/fundamentals/error-handling
if (env.IsDevelopment()) app.UseDeveloperExceptionPage();
// to serve up index.html
app.UseDefaultFiles();
app.UseStaticFiles();
// CORS
// https://docs.asp.net/en/latest/security/cors.html
app.UseCors(builder =>
builder.WithOrigins("http://localhost:4200")
.AllowAnyHeader()
.AllowAnyMethod());
// add a middleware used to validate access tokens and protect the API endpoints
app.UseOAuthValidation();
app.UseOpenIddict();
app.UseMvc();
// app.UseMvcWithDefaultRoute();
// app.UseWelcomePage();
// seed the database
databaseInitializer.Seed().GetAwaiter().GetResult();
// swagger
// enable middleware to serve generated Swagger as a JSON endpoint
app.UseSwagger();
// enable middleware to serve swagger-ui assets (HTML, JS, CSS etc.)
app.UseSwaggerUi();
}
And then my controller (you can find the whole solution in the repository quoted above):
public sealed class AuthorizationController : Controller
{
private readonly IOptions<IdentityOptions> _identityOptions;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly UserManager<ApplicationUser> _userManager;
public AuthorizationController(
IOptions<IdentityOptions> identityOptions,
SignInManager<ApplicationUser> signInManager,
UserManager<ApplicationUser> userManager)
{
_identityOptions = identityOptions;
_signInManager = signInManager;
_userManager = userManager;
}
private async Task<AuthenticationTicket> CreateTicketAsync(OpenIdConnectRequest request, ApplicationUser user)
{
// Create a new ClaimsPrincipal containing the claims that
// will be used to create an id_token, a token or a code.
ClaimsPrincipal principal = await _signInManager.CreateUserPrincipalAsync(user);
// Create a new authentication ticket holding the user identity.
AuthenticationTicket ticket = new AuthenticationTicket(
principal, new AuthenticationProperties(),
OpenIdConnectServerDefaults.AuthenticationScheme);
// Set the list of scopes granted to the client application.
// Note: the offline_access scope must be granted
// to allow OpenIddict to return a refresh token.
ticket.SetScopes(new[] {
OpenIdConnectConstants.Scopes.OpenId,
OpenIdConnectConstants.Scopes.Email,
OpenIdConnectConstants.Scopes.Profile,
OpenIdConnectConstants.Scopes.OfflineAccess,
OpenIddictConstants.Scopes.Roles
}.Intersect(request.GetScopes()));
ticket.SetResources("resource-server");
// Note: by default, claims are NOT automatically included in the access and identity tokens.
// To allow OpenIddict to serialize them, you must attach them a destination, that specifies
// whether they should be included in access tokens, in identity tokens or in both.
foreach (var claim in ticket.Principal.Claims)
{
// Never include the security stamp in the access and identity tokens, as it's a secret value.
if (claim.Type == _identityOptions.Value.ClaimsIdentity.SecurityStampClaimType)
continue;
List<string> destinations = new List<string>
{
OpenIdConnectConstants.Destinations.AccessToken
};
// Only add the iterated claim to the id_token if the corresponding scope was granted to the client application.
// The other claims will only be added to the access_token, which is encrypted when using the default format.
if (claim.Type == OpenIdConnectConstants.Claims.Name &&
ticket.HasScope(OpenIdConnectConstants.Scopes.Profile) ||
claim.Type == OpenIdConnectConstants.Claims.Email &&
ticket.HasScope(OpenIdConnectConstants.Scopes.Email) ||
claim.Type == OpenIdConnectConstants.Claims.Role &&
ticket.HasScope(OpenIddictConstants.Claims.Roles))
{
destinations.Add(OpenIdConnectConstants.Destinations.IdentityToken);
}
claim.SetDestinations(OpenIdConnectConstants.Destinations.AccessToken);
}
return ticket;
}
[HttpPost("~/connect/token"), Produces("application/json")]
public async Task<IActionResult> Exchange(OpenIdConnectRequest request)
{
// if you prefer not to bind the request as a parameter, you can still use:
// OpenIdConnectRequest request = HttpContext.GetOpenIdConnectRequest();
Debug.Assert(request.IsTokenRequest(),
"The OpenIddict binder for ASP.NET Core MVC is not registered. " +
"Make sure services.AddOpenIddict().AddMvcBinders() is correctly called.");
if (!request.IsPasswordGrantType())
{
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.UnsupportedGrantType,
ErrorDescription = "The specified grant type is not supported."
});
}
ApplicationUser user = await _userManager.FindByNameAsync(request.Username);
if (user == null)
{
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The username/password couple is invalid."
});
}
// Ensure the user is allowed to sign in.
if (!await _signInManager.CanSignInAsync(user))
{
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The specified user is not allowed to sign in."
});
}
// Reject the token request if two-factor authentication has been enabled by the user.
if (_userManager.SupportsUserTwoFactor && await _userManager.GetTwoFactorEnabledAsync(user))
{
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The specified user is not allowed to sign in."
});
}
// Ensure the user is not already locked out.
if (_userManager.SupportsUserLockout && await _userManager.IsLockedOutAsync(user))
{
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The username/password couple is invalid."
});
}
// Ensure the password is valid.
if (!await _userManager.CheckPasswordAsync(user, request.Password))
{
if (_userManager.SupportsUserLockout)
await _userManager.AccessFailedAsync(user);
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The username/password couple is invalid."
});
}
if (_userManager.SupportsUserLockout)
await _userManager.ResetAccessFailedCountAsync(user);
// Create a new authentication ticket.
AuthenticationTicket ticket = await CreateTicketAsync(request, user);
var result = SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
return result;
// return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
}
[HttpGet("~/connect/logout")]
public async Task<IActionResult> Logout()
{
// Extract the authorization request from the ASP.NET environment.
OpenIdConnectRequest request = HttpContext.GetOpenIdConnectRequest();
// Ask ASP.NET Core Identity to delete the local and external cookies created
// when the user agent is redirected from the external identity provider
// after a successful authentication flow (e.g Google or Facebook).
await _signInManager.SignOutAsync();
// Returning a SignOutResult will ask OpenIddict to redirect the user agent
// to the post_logout_redirect_uri specified by the client application.
return SignOut(OpenIdConnectServerDefaults.AuthenticationScheme);
}
// http://openid.net/specs/openid-connect-core-1_0.html#UserInfo
[Authorize]
[HttpGet("~/connect/userinfo")]
public async Task<IActionResult> GetUserInfo()
{
ApplicationUser user = await _userManager.GetUserAsync(User);
// to simplify, in this demo we just have 1 role for users: either admin or editor
string sRole = await _userManager.IsInRoleAsync(user, "admin")
? "admin"
: "editor";
// http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
return Ok(new
{
sub = user.Id,
given_name = user.FirstName,
family_name = user.LastName,
name = user.UserName,
user.Email,
email_verified = user.EmailConfirmed,
roles = sRole
});
}
}
As mentioned in this blog post, the token format used by OpenIddict slightly changed recently, which makes tokens issued by the latest OpenIddict bits incompatible with the old OAuth2 validation middleware version you're using.
Migrate to AspNet.Security.OAuth.Validation 1.0.0 and it should work.