Good afternoon,
So I am working on integrating OpenIddict into an existing project. I have become stuck on the issuing of the token because I am unable to debug the issue. So I am getting a two entirely different responses from the controller. I have tried this through swagger and postman both and gotten the same Status: 415 Unsupported Media Type. Now I was pretty sure I read that the information coming to the token for the password grant had to be "application/x-www-form-urlencoded" So that is what I am passing to the controller, but still receiving a 415. On the flip side of that if I look in the debug log for the project I see the following:
OpenIddict.Server.OpenIddictServerDispatcher: Information: The request address matched a server endpoint: Token.
OpenIddict.Server.OpenIddictServerDispatcher: Information: The token request was successfully extracted: {
"grant_type": "password",
"username": "Administrator@MRM2Inc.com",
"password": "[redacted]"
}.
OpenIddict.Server.OpenIddictServerDispatcher: Information: The token request was successfully validated.
That too me looks like it was working. It never went into the token endpoint as I had a breakpoint immediately set. Here is my setup:
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddDbContext<IdentDbContext>(options =>
{
//options.UseSqlServer(
// Configuration.GetConnectionString("IdentityDB"));
options.UseOpenIddict<Guid>();
});
// Add the Identity Services we are going to be using the Application Users and the Application Roles
services.AddIdentity<ApplicationUsers, ApplicationRoles>(config =>
{
config.SignIn.RequireConfirmedEmail = true;
config.SignIn.RequireConfirmedAccount = true;
config.User.RequireUniqueEmail = true;
config.Lockout.MaxFailedAccessAttempts = 3;
}).AddEntityFrameworkStores<IdentDbContext>()
.AddUserStore<ApplicationUserStore>()
.AddRoleStore<ApplicationRoleStore>()
.AddRoleManager<ApplicationRoleManager>()
.AddUserManager<ApplicationUserManager>()
.AddErrorDescriber<ApplicationIdentityErrorDescriber>()
.AddDefaultTokenProviders()
.AddDefaultUI();
services.AddDataLibrary();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
});
//services.AddSingleton<IGenerateTokens, GenerateTokens>();
// Add in the email
var emailConfig = Configuration.GetSection("EmailConfiguration").Get<EmailConfiguration>();
services.AddSingleton(emailConfig);
services.AddEmailLibrary();
services.AddOpenIddict()
// Register the OpenIddict core components.
.AddCore(options =>
{
// Configure OpenIddict to use the Entity Framework Core stores and models.
// Note: call ReplaceDefaultEntities() to replace the default entities.
options.UseEntityFrameworkCore()
.UseDbContext<IdentDbContext>()
.ReplaceDefaultEntities<Guid>();
})
// Register the OpenIddict server components.
.AddServer(options =>
{
// Enable the token endpoint. What other endpoints?
options.SetTokenEndpointUris("/Token");
// Enable the client credentials flow. Which flow do I need?
options.AllowPasswordFlow();
options.AcceptAnonymousClients();
// Register the signing and encryption credentials.
options.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate();
// Register the ASP.NET Core host and configure the ASP.NET Core options.
options.UseAspNetCore()
.EnableTokenEndpointPassthrough();
})
// Register the OpenIddict validation components.
.AddValidation(options =>
{
// Import the configuration from the local OpenIddict server instance.
options.UseLocalServer();
// Register the ASP.NET Core host.
options.UseAspNetCore();
});
// Register the Swagger generator, defining 1 or more Swagger documents
services.AddSwaggerGen(swagger =>
{
swagger.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme()
{
Name = "Authorization",
Type = SecuritySchemeType.Http,
Scheme = "Bearer",
BearerFormat = "JWT",
In = ParameterLocation.Header,
Description = "JWT Authorization header using the Bearer scheme. \r\n\r\n Enter 'Bearer'[space] and then your token in the text input below.\r\n\r\nExample: \"Bearer 12345abcdef\""
});
swagger.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
new string[] {}
}
});
swagger.OperationFilter<SwaggerDefaultValues>();
swagger.OperationFilter<AuthenticationRequirementOperationFilter>();
// Set the comments path for the Swagger JSON and UI.
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
swagger.IncludeXmlComments(xmlPath);
});
services.AddApiVersioning();
services.AddVersionedApiExplorer(options =>
{
options.GroupNameFormat = "'v'VVVV";
options.DefaultApiVersion = ApiVersion.Parse("0.6.alpha");
options.AssumeDefaultVersionWhenUnspecified = true;
});
services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider provider)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
// Enable middleware to serve generated Swagger as a JSON endpoint.
app.UseSwagger();
// Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.),
// specifying the Swagger JSON endpoint.
app.UseSwaggerUI(c =>
{
c.DisplayOperationId();
var versionDescription = provider.ApiVersionDescriptions;
foreach (var description in provider.ApiVersionDescriptions.OrderByDescending(_ => _.ApiVersion))
{
c.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", $"MRM2 Identity API {description.GroupName}");
}
});
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
AuthorizationController.cs:
[Route("api/[controller]/[action]")]
[ApiController]
[ApiVersion("0.8.alpha")]
[Produces(MediaTypeNames.Application.Json)]
[Consumes(MediaTypeNames.Application.Json)]
public class AuthorizationController : ControllerBase
{
private readonly IConfiguration _configuration;
private readonly IdentDbContext _context;
private readonly ApplicationUserManager _userManager;
private readonly ApplicationRoleManager _roleManager;
private readonly IGenerateTokens _tokens;
private readonly IOpenIddictApplicationManager _applicationManager;
private readonly IOpenIddictAuthorizationManager _authorizationManager;
private readonly IOpenIddictScopeManager _scopeManager;
private readonly SignInManager<ApplicationUsers> _signInManager;
private HttpClient _client;
public AuthorizationController(IConfiguration configuration, IdentDbContext context, ApplicationUserManager userManager,
ApplicationRoleManager roleManager, IGenerateTokens tokens, IOpenIddictApplicationManager applicationManager, IOpenIddictAuthorizationManager authorizationManager,
IOpenIddictScopeManager scopeManager, SignInManager<ApplicationUsers> signInManager)
{
_configuration = configuration;
_context = context;
_userManager = userManager;
_roleManager = roleManager;
_tokens = tokens;
_applicationManager = applicationManager;
_authorizationManager = authorizationManager;
_scopeManager = scopeManager;
_signInManager = signInManager;
}
[HttpPost("/token"), Produces("application/json")]
public async Task<IActionResult> Exchange()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved."); //does not even hit this breakpoint.
ClaimsPrincipal claimsPrincipal;
if (request.IsPasswordGrantType())
{
var user = await _userManager.FindByNameAsync(request.Username);
var roleList = await _userManager.GetRolesListAsync(user);
var databaseList = await _userManager.GetDatabasesAsync(user);
string symKey = _configuration["Jwt:Symmetrical:Key"];
string jwtSub = _configuration["Jwt:Subject"];
string issuer = _configuration["Jwt:Issuer"];
string audience = _configuration["Jwt:Audience"];
var claims = new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Sub, jwtSub, issuer),
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString(), issuer),
new Claim(ClaimTypes.Name, user.UserName, issuer)
};
foreach (var role in roleList)
{
claims.Add(new Claim(ClaimTypes.Role, role.Name));
}
foreach (var database in databaseList)
{
claims.Add(new Claim(type: "DatabaseName", database));
}
var identity = new ClaimsIdentity(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
identity.AddClaim(OpenIddictConstants.Claims.Name, user.UserName, OpenIddictConstants.Destinations.AccessToken);
identity.AddClaim(OpenIddictConstants.Claims.Subject, jwtSub, OpenIddictConstants.Destinations.AccessToken);
identity.AddClaim(OpenIddictConstants.Claims.Audience, audience, OpenIddictConstants.Destinations.AccessToken);
foreach (var cl in claims)
{
identity.AddClaim(cl.Type, cl.Value, OpenIddictConstants.Destinations.AccessToken);
}
claimsPrincipal = new ClaimsPrincipal(identity);
}
if (request.IsAuthorizationCodeGrantType() || request.IsRefreshTokenGrantType())
{
// Retrieve the claims principal stored in the authorization code/device code/refresh token.
var principal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal;
// Retrieve the user profile corresponding to the authorization code/refresh token.
// Note: if you want to automatically invalidate the authorization code/refresh token
// when the user password/roles change, use the following line instead:
// var user = _signInManager.ValidateSecurityStampAsync(info.Principal);
var user = await _userManager.GetUserAsync(principal);
if (user == null)
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The token is no longer valid."
}));
}
// Ensure the user is still allowed to sign in.
if (!await _signInManager.CanSignInAsync(user))
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is no longer allowed to sign in."
}));
}
foreach (var claim in principal.Claims)
{
claim.SetDestinations(GetDestinations(claim, principal));
}
// Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
throw new InvalidOperationException("The specified grant type is not supported.");
}
private IEnumerable<string> GetDestinations(Claim claim, ClaimsPrincipal principal)
{
// 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.
switch (claim.Type)
{
case Claims.Name:
yield return Destinations.AccessToken;
if (principal.HasScope(Scopes.Profile))
yield return Destinations.IdentityToken;
yield break;
case Claims.Email:
yield return Destinations.AccessToken;
if (principal.HasScope(Scopes.Email))
yield return Destinations.IdentityToken;
yield break;
case Claims.Role:
yield return Destinations.AccessToken;
if (principal.HasScope(Scopes.Roles))
yield return Destinations.IdentityToken;
yield break;
// Never include the security stamp in the access and identity tokens, as it's a secret value.
case "AspNet.Identity.SecurityStamp": yield break;
default:
yield return Destinations.AccessToken;
yield break;
}
}
}
Here is how I have postman set for this, you can see the 415 return
Not sure what I missed in the setup, but I am receiving conflicting information, plus I want to troubleshoot the controller to determine what else I need to finish off the controller and ensure I am getting the information that I is needed for the rest of the program. The 415 is not allowing me to troubleshoot the issue, but something is saying that the information is correct?
Turns out the issue was at the head of the controller. The previous answer helped me see that while they saw the produces("application/json"), it was not exactly that. At the top of the controller I had a Consumes("application/json"). Removed this and it went into the method.