Zafrul Hassan Zafrul Hassan - 21 days ago 8
ASP.NET (C#) Question

OpenIDDict in asp.net core error " The OpenID Connect request cannot be retrieved"


System.InvalidOperationException: The OpenID Connect request cannot be
retrieved from the ASP.NET context. Make sure that
'app.UseOpenIddict()' is called before 'app.UseMvc()' and that the
action route corresponds to the endpoint path registered via
'services.AddOpenIddict().Enable[...]Endpoint(...)'. at
OpenIddict.Mvc.OpenIddictModelBinder.BindModelAsync(ModelBindingContext
context)


MyStartup.cs

public void ConfigureServices(IServiceCollection services)
{
services.AddApplicationInsightsTelemetry(Configuration);
services.AddDbContext<ApplicationUserDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

services.AddIdentity<ApplicationUser, ApplicationRole>()
.AddEntityFrameworkStores<ApplicationUserDbContext>()
.AddDefaultTokenProviders();

services.AddMvc();



.AddMvcBinders()

.EnableAuthorizationEndpoint("/connect/authorize")
.EnableLogoutEndpoint("/connect/logout")
.EnableTokenEndpoint("/connect/token")
.EnableUserinfoEndpoint("/Account/Userinfo")


.AllowAuthorizationCodeFlow()
.AllowPasswordFlow()
.AllowRefreshTokenFlow()


.RequireClientIdentification()

// During development, you can disable the HTTPS requirement.
.DisableHttpsRequirement()

.AddEphemeralSigningKey();

services.AddTransient<IEmailSender, AuthMessageSender>();
services.AddTransient<ISmsSender, AuthMessageSender>();
}


public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();

app.UseApplicationInsightsRequestTelemetry();

if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}

app.UseApplicationInsightsExceptionTelemetry();

app.UseStaticFiles();

app.UseCsp(options => options.DefaultSources(directive => directive.Self())
.ImageSources(directive => directive.Self()
.CustomSources("*"))
.ScriptSources(directive => directive.Self()
.UnsafeInline())
.StyleSources(directive => directive.Self()
.UnsafeInline()));

app.UseXContentTypeOptions();

app.UseXfo(options => options.Deny());

app.UseXXssProtection(options => options.EnabledWithBlockMode());

app.UseIdentity();

// Add a middleware used to validate access
// tokens and protect the API endpoints.
app.UseOAuthValidation();

app.UseGoogleAuthentication(new GoogleOptions
{

});

app.UseStatusCodePagesWithReExecute("/error");

app.UseOpenIddict();

app.UseMvcWithDefaultRoute();
}


Update



using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Extensions;
using AspNet.Security.OpenIdConnect.Server;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Mvc.Server.Models;
using Mvc.Server.ViewModels.Authorization;
using Mvc.Server.ViewModels.Shared;
using OpenIddict;

public class AuthorizationController : Controller {
private readonly OpenIddictApplicationManager<OpenIddictApplication> _applicationManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly UserManager<ApplicationUser> _userManager;

public AuthorizationController(
OpenIddictApplicationManager<OpenIddictApplication> applicationManager,
SignInManager<ApplicationUser> signInManager,
UserManager<ApplicationUser> userManager) {
_applicationManager = applicationManager;
_signInManager = signInManager;
_userManager = userManager;
}

// Note: to support interactive flows like the code flow,
// you must provide your own authorization endpoint action:

[Authorize, HttpGet, Route("~/connect/authorize")]
public async Task<IActionResult> Authorize(OpenIdConnectRequest request) {
// Retrieve the application details from the database.
var application = await _applicationManager.FindByClientIdAsync(request.ClientId);
if (application == null) {
return View("Error", new ErrorViewModel {
Error = OpenIdConnectConstants.Errors.InvalidClient,
ErrorDescription = "Details concerning the calling client application cannot be found in the database"
});
}

// Flow the request_id to allow OpenIddict to restore
// the original authorization request from the cache.
return View(new AuthorizeViewModel {
ApplicationName = application.DisplayName,
RequestId = request.RequestId,
Scope = request.Scope
});
}

[Authorize, HttpPost("~/connect/authorize/accept"), ValidateAntiForgeryToken]
public async Task<IActionResult> Accept(OpenIdConnectRequest request) {
// Retrieve the profile of the logged in user.
var user = await _userManager.GetUserAsync(User);
if (user == null) {
return View("Error", new ErrorViewModel {
Error = OpenIdConnectConstants.Errors.ServerError,
ErrorDescription = "An internal error has occurred"
});
}

// Create a new authentication ticket.
var ticket = await CreateTicketAsync(request, user);

// Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
}

[Authorize, HttpPost("~/connect/authorize/deny"), ValidateAntiForgeryToken]
public IActionResult Deny() {
// Notify OpenIddict that the authorization grant has been denied by the resource owner
// to redirect the user agent to the client application using the appropriate response_mode.
return Forbid(OpenIdConnectServerDefaults.AuthenticationScheme);
}

// Note: the logout action is only useful when implementing interactive
// flows like the authorization code flow or the implicit flow.

[HttpGet("~/connect/logout")]
public IActionResult Logout(OpenIdConnectRequest request) {
// Flow the request_id to allow OpenIddict to restore
// the original logout request from the distributed cache.
return View(new LogoutViewModel {
RequestId = request.RequestId
});
}

[HttpPost("~/connect/logout"), ValidateAntiForgeryToken]
public async Task<IActionResult> Logout() {
// 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);
}

// Note: to support non-interactive flows like password,
// you must provide your own token endpoint action:

[HttpPost("~/connect/token")]
[Produces("application/json")]
public async Task<IActionResult> Exchange(OpenIdConnectRequest request) {
if (request.IsPasswordGrantType()) {
var 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.
var ticket = await CreateTicketAsync(request, user);

return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
}

return BadRequest(new OpenIdConnectResponse {
Error = OpenIdConnectConstants.Errors.UnsupportedGrantType,
ErrorDescription = "The specified grant type is not supported."
});
}

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.
var principal = await _signInManager.CreateUserPrincipalAsync(user);

// 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 principal.Claims) {
// In this sample, every claim is serialized in both the access and the identity tokens.
// In a real world application, you'd probably want to exclude confidential claims
// or apply a claims policy based on the scopes requested by the client application.
claim.SetDestinations(OpenIdConnectConstants.Destinations.AccessToken,
OpenIdConnectConstants.Destinations.IdentityToken);
}

// Create a new authentication ticket holding the user identity.
var 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()));

return ticket;
}
}


Changed:

[Authorize, HttpPost("~/connect/authorize"), ValidateAntiForgeryToken]
public async Task<IActionResult> Accept(OpenIdConnectRequest request) {
// Retrieve the profile of the logged in user.
var user = await _userManager.GetUserAsync(User);
if (user == null) {
return View("Error", new ErrorViewModel {
Error = OpenIdConnectConstants.Errors.ServerError,
ErrorDescription = "An internal error has occurred"
});
}

// Create a new authentication ticket.
var ticket = await CreateTicketAsync(request, user);

// Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
}

Answer

OpenIddict used to allow "subroutes" like /connect/authorize/accept or /connect/authorize/deny to be recognized as valid authorization endpoint paths when /connect/authorize was specified, but this feature was removed recently.

With the latest OpenIddict bits, you're encouraged to use the same route template for all your authorization endpoint actions.

[Authorize, HttpGet("~/connect/authorize")]
public async Task<IActionResult> Authorize(OpenIdConnectRequest request) {
    // ...
}

[Authorize, FormValueRequired("submit.Accept")]
[HttpPost("~/connect/authorize"), ValidateAntiForgeryToken]
public async Task<IActionResult> Accept(OpenIdConnectRequest request) {
    // ...
}

[Authorize, FormValueRequired("submit.Deny")]
[HttpPost("~/connect/authorize"), ValidateAntiForgeryToken]
public IActionResult Deny() {
    // ...
}

You can use Orchard's [FormValueRequired] approach to discriminate your actions:

public sealed class FormValueRequiredAttribute : ActionMethodSelectorAttribute {
    private readonly string _name;

    public FormValueRequiredAttribute(string name) {
        _name = name;
    }

    public override bool IsValidForRequest(RouteContext context, ActionDescriptor action) {
        if (string.Equals(context.HttpContext.Request.Method, "GET", StringComparison.OrdinalIgnoreCase) ||
            string.Equals(context.HttpContext.Request.Method, "HEAD", StringComparison.OrdinalIgnoreCase) ||
            string.Equals(context.HttpContext.Request.Method, "DELETE", StringComparison.OrdinalIgnoreCase) ||
            string.Equals(context.HttpContext.Request.Method, "TRACE", StringComparison.OrdinalIgnoreCase)) {
            return false;
        }

        if (string.IsNullOrEmpty(context.HttpContext.Request.ContentType)) {
            return false;
        }

        if (!context.HttpContext.Request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)) {
            return false;
        }

        return !string.IsNullOrEmpty(context.HttpContext.Request.Form[_name]);
    }
}

Don't forget to also update your submit buttons:

<input class="btn btn-lg btn-success" name="submit.Accept" type="submit" value="Yes" />
<input class="btn btn-lg btn-danger" name="submit.Deny" type="submit" value="No" />
Comments