0

The current follow of my application is that I'm getting access-token from the front-end then using that token my backend is calling the External-Login provider to conform the user identity and retrieve the extra information-from the login provider.

As in this method I have to setup and configure login provider for each client(web ,android ). I'm tryin to use Microsoft.AspNetCore.Authentication.Facebook in my back-end to handle the facebook login using the claims which I'm getting from facebook I want to use that to generate Openidic JWT token. Everything works fine but statement

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

is throwing an Exception

An authorization or token response cannot be returned from this endpoint.

here is the question with same error but context is different ASP.NET Core Openiddict throws "An OpenID Connect response cannot be returned from this endpoint"

here is another question with similar approach but different How to Generate AccessToken for user who is logged in with External Providers

Here is my startup.cs code

 public void ConfigureServices(IServiceCollection services)
{
    var connectionString = Configuration["ConnectionStrings:ApplicationDbContext"];

    services.AddEntityFrameworkNpgsql();

    services.AddDbContext<ApplicationDbContext>(
        opts =>
        {
            opts.UseNpgsql(connectionString, b => b.MigrationsAssembly("Data.Repository"));
            opts.UseOpenIddict();
        }
    );

    services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();

            services.AddAuthentication().AddFacebook(facebookOptions =>
            {
                facebookOptions.AppId = "APP_ID";
                facebookOptions.AppSecret = "APP_SECRET";
                facebookOptions.SaveTokens = true;

            });

    var validIssuer = Configuration["Token:Issuer"];
    services.AddAuthentication()
        .AddJwtBearer(cfg =>
        {
            cfg.TokenValidationParameters = new TokenValidationParameters
            {
                ValidIssuer = validIssuer,
                IssuerSigningKey = securityKey,

                ValidateIssuer = !String.IsNullOrEmpty(validIssuer),
                ValidateAudience = false,
                ValidateLifetime = true,
                ValidateActor = false,
                ValidateIssuerSigningKey = true
            };
        });

    services.AddOpenIddict(options =>
    {
        // Register the Entity Framework stores.
        options.AddEntityFrameworkCoreStores<ApplicationDbContext>();
        options.AddMvcBinders();
        options.EnableTokenEndpoint("/api/account/token");
        options.UseJsonWebTokens();
        options.AllowPasswordFlow();
        options.AllowCustomFlow("urn:ietf:params:oauth:grant-type:facebook_access_token");
        options.AllowCustomFlow("urn:ietf:params:oauth:grant-type:google_access_token");
        options.AllowCustomFlow("urn:ietf:params:oauth:grant-type:microsoft_access_token");
        options.DisableHttpsRequirement();
        options.AddSigningKey(securityKey);

    });


    services.Configure<IdentityOptions>(options =>
    {

    //OpenId

        options.ClaimsIdentity.UserNameClaimType = OpenIdConnectConstants.Claims.Name;
        options.ClaimsIdentity.UserIdClaimType = OpenIdConnectConstants.Claims.Subject;
        options.ClaimsIdentity.RoleClaimType = OpenIdConnectConstants.Claims.Role;

        // Password settings
        options.Password.RequireDigit = true;
        options.Password.RequiredLength = 8;
        options.Password.RequireNonAlphanumeric = false;
        options.Password.RequireUppercase = true;
        options.Password.RequireLowercase = false;

        // Lockout settings
        options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
        options.Lockout.MaxFailedAccessAttempts = 10;

        // User settings
        options.User.RequireUniqueEmail = true;
    });


    services.AddCors(o => o.AddPolicy("CorsPolicy", builder =>
    {
        builder.AllowAnyOrigin()
                .AllowAnyMethod()
                .AllowAnyHeader();
    }));

    // Add framework services.
    services.AddMvc(options =>
    {
        options.Filters.Add(new GlobalExceptionFilter());
    });   

    services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public virtual void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, RoleManager<IdentityRole> roleManager)
{
    loggerFactory.AddConsole(Configuration.GetSection("Logging"));
    loggerFactory.AddDebug();

    app.UseAuthentication();
    app.UseCors("CorsPolicy");

    app.UseMvc();
}

This is the controller which is handling External-Login

   [HttpGet]
    [AllowAnonymous]
    public IActionResult ExternalLogin(string provider = "Facebook", string returnUrl = null)
    {
        // Request a redirect to the external login provider.
        var redirectUrl = Url.Action(nameof(ExternalLoginCallback), "Auth", new { returnUrl });
        var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
        return Challenge(properties, provider);
    }

    [HttpGet]
    [AllowAnonymous]
    public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null)
    {
        if (remoteError != null)
        {
            return BadRequest("Error from external provider");
        }
        var info = await _signInManager.GetExternalLoginInfoAsync();

        if (info == null)
        {
            return BadRequest();
        }
        try
        {
            var ticket = await _accountService.TokenExchangeAsync(info);
            return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);

        }
        catch (Exception ex)
        {
            return BadRequest(new OpenIdConnectResponse
            {
                Error = OpenIdConnectConstants.Errors.ServerError,
                ErrorDescription = ex.Message
            });
        }
    }

Here the code that handles creating user and other related stuff and it is called by ExternalLoginCallback

    public async Task<AuthenticationTicket> TokenExchangeAsync (ExternalLoginInfo info)
        {
            var claims = info.Principal.Claims;
            var profile = new Profile
            {
                email = claims.FirstOrDefault(x => x.Type == ClaimTypes.Email).Value,
                id = claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier).Value,
                name = claims.FirstOrDefault(x => x.Type == ClaimTypes.Name).Value
            };

            // Find the user
            var user = await _userManager.FindByLoginAsync(info.LoginProvider, profile.id);
            if (user == null)
            {
                if (string.IsNullOrEmpty(profile.email))
                    throw new Exception("Email is not specified in the user profile for the provider.");

                await RegisterExtenalUserAsync(profile, info.LoginProvider);

                /// Try to find the user
                user = await _userManager.FindByLoginAsync(info.LoginProvider, profile.id);
                if (user == null)
                {
                    // Return bad request if the user doesn't exist
                    throw new Exception("Invalid profile or provider.");
                }
            }
            // Create the principal
            var principal = await _signInManager.CreateUserPrincipalAsync(user);

            // Claims will not be associated with specific destinations by default, so we must indicate whether they should
            // be included or not in access and identity tokens.
            foreach (var claim in principal.Claims)
            {
                // For this sample, just include all claims in all token types.
                // In reality, claims' destinations would probably differ by token type and depending on the scopes requested.
                claim.SetDestinations(OpenIdConnectConstants.Destinations.AccessToken, OpenIdConnectConstants.Destinations.IdentityToken);
            }

            // Create a new authentication ticket for the user's principal
            var ticket = new AuthenticationTicket(principal, new AuthenticationProperties(),
               OpenIdConnectServerDefaults.AuthenticationScheme);

  //  Include resources and scopes, as appropriate
          var scope = new[]
            {
                    OpenIdConnectConstants.Scopes.OpenId,
                    OpenIdConnectConstants.Scopes.Email,
                    OpenIdConnectConstants.Scopes.Profile,
                    OpenIddictConstants.Scopes.Roles
            };

            ticket.SetScopes(scope);

            // Sign in the user
            return ticket;
        }

Here is the Log file

    Project> info: Microsoft.AspNetCore.Mvc.SignInResult[1]
Project>       Executing SignInResult with authentication scheme (ASOS) and the following principal: System.Security.Claims.ClaimsPrincipal.
Project> info: Microsoft.AspNetCore.Mvc.SignInResult[1]
Project>       Executing SignInResult with authentication scheme (ASOS) and the following principal: System.Security.Claims.ClaimsPrincipal.
Project> info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2]
Project>       Executed action Project.Controllers.AuthController.ExternalLoginCallback (Project) in 9068.4071ms
Project> info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2]
Project>       Executed action Project.Controllers.AuthController.ExternalLoginCallback (Project) in 9068.4071ms
Project> fail: Microsoft.AspNetCore.Server.Kestrel[13]
Project>       Connection id "0HL86DQMN0Q30", Request id "0HL86DQMN0Q30:00000003": An unhandled exception was thrown by the application.
Project> System.InvalidOperationException: An authorization or token response cannot be returned from this endpoint.
Project>    at AspNet.Security.OpenIdConnect.Server.OpenIdConnectServerHandler.<SignInAsync>d__6.MoveNext()
Project> --- End of stack trace from previous location where exception was thrown ---

Any help will be appreciated!

Bipn Paul
  • 1,415
  • 2
  • 14
  • 24
  • 1
    @poke `SignIn` is a `ControllerBase` method and is one of the entry points of the ASP.NET Core authentication stack in MVC Core (the other ones are `SignOut`, `Challenge` and `Forbid`). Here, the OP is implementing an OIDC server using OpenIddict and implementing the logic in an authorization controller is indeed the recommended option. – Kévin Chalet Sep 29 '17 at 04:13
  • 1
    @Pinpoint Ah, okay, didn’t realize this was a *server* and not an application. Thanks! – poke Sep 29 '17 at 08:08

1 Answers1

2

Your question is actually really similar to the second link you posted and the exception you're seeing has the same root cause: you're trying to return a token response from an endpoint that is not considered as your token endpoint by OpenIddict (i.e the path of your ExternalLoginCallback action doesn't match the one set when calling options.EnableTokenEndpoint("/path").

For security reasons, OpenIddict refuses to process such a response and throws an exception.

To fix that, you'll have to do 2 things:

Use code or implicit instead of the password flow

The password flow can't be used with interactive authentication flows that involves roundtrips to external providers like Facebook (you can see this flow as a simple API call that takes your username/password and returns a token).

Instead, consider using an interactive flow like code (for mobile apps) or implicit (for browser-based apps). You can find samples for these 2 flows here: https://github.com/openiddict/openiddict-samples/tree/dev/samples

For instance, to enable implicit flow support, you can do something like that:

// 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.
    options.AddMvcBinders();

    // Enable the authorization endpoint.
    options.EnableAuthorizationEndpoint("/connect/authorize");

    // Enable implicit flow support.
    options.AllowImplicitFlow();

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

    // Register a new ephemeral key, that is discarded when the application
    // shuts down. Tokens signed using this key are automatically invalidated.
    // This method should only be used during development.
    options.AddEphemeralSigningKey();
});

Move the SignIn logic to avoid calling it from endpoints that are not managed by OpenIddict

Since you can't call SignIn from ExternalLoginCallback, you'll have to move the SignIn call to your authorization endpoint action (in the OpenIddict samples, it's generally named AuthorizationController.Authorize()):

var ticket = await _accountService.TokenExchangeAsync(info);
return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);

Then, revert the changes made in your ExternalLoginCallback action so it simply redirects the user to your Authorize action when the FB login process is complete (don't forget that here, the returnUrl parameter represents the location of your authorization endpoint with the mandatory OIDC parameters):

return LocalRedirect(returnUrl);

If things are implemented properly, the flow will be:

Client application -> authorization endpoint -> login page -> Facebook -> external login callback -> authorization endpoint -> client application.
Kévin Chalet
  • 39,509
  • 7
  • 121
  • 131