openiddict / openiddict-core

Flexible and versatile OAuth 2.0/OpenID Connect stack for .NET

Home Page:https://documentation.openiddict.com/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Bearer not working?

Gillardo opened this issue · comments

I have setup my code as stated on the readme file, but when i call my resourceController i get an error saying Authorization failed for the request filter.

The only changes i have done to use Bearer is thus

app.UseOpenIddict(options =>
{
    // Need this line to use Bearer Authorization in requests
    options.Options.AuthenticationScheme = OAuthValidationDefaults.AuthenticationScheme;

    // development
   options.Options.AllowInsecureHttp = true;
});

My resourceController looks like so

    public class ResourceController : Controller {
        [Authorize(ActiveAuthenticationSchemes = OAuthValidationDefaults.AuthenticationScheme)]
        [HttpGet("message")]
        public IActionResult GetMessage() {
            var identity = User.Identity as ClaimsIdentity;
            if (identity == null) {
                return HttpBadRequest();
            }

            return Content($"{identity.Name} has been successfully authenticated.");
        }
    }

To call this, i call http://localhost:5000/connect/token with a valid username and password, and then using the accessToken string returned, i call http://localhost/resource/message. An example of the call is like so

GET /api/message HTTP/1.1
Host: localhost:5000
Authorization: Bearer BIG_STRING_HERE
Cache-Control: no-cache

I have also tried adding JwtTokens, but no luck as still fails. All my code is the same as the readme, apart from above.

here is my whole startup.cs file

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.AuthenticationDescriptions.Clear();
        options.FlowWindowsAuthentication = false;
    });

    app.UseOverrideHeaders(options => {
        options.ForwardedOptions = ForwardedHeaders.All;
    });

    app.UseStaticFiles();

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

    // Note: OpenIddict must be added after
    // ASP.NET Identity and the external providers.
    app.UseOpenIddict(options =>
    {
        // Need this line to use Bearer Authorization in requests
        options.Options.AuthenticationScheme = OAuthValidationDefaults.AuthenticationScheme;

        // development
        options.Options.AllowInsecureHttp = true;
    });

    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();
        }
    }
}

As mentioned in the sample, you need another middleware to validate the bearer tokens: https://github.com/openiddict/core/blob/dev/samples/Mvc.Server/Startup.cs#L64-L66

Add the validation middleware, remove options.Options.AuthenticationScheme = OAuthValidationDefaults.AuthenticationScheme;and it should work.

This worked perfectly, sorry for the stupid question. Since you answered that question so well :), could you help me with another problem? When a user is authenticated by an external provider (lets say google), how do you get a local access_token for the user once they have registered (in the ExternalLoginConfirmation method of the AccountController)??

You need to redirect the user agent back to the authorization endpoint (contained in the returnUrl parameter): http://stackoverflow.com/a/34821722/542757

@PinpointTownes really appreicate your help on this, that was my post on stackoverflow, so thanks, but still not working. So, as you suggested, i have reverted my AccountController to be the same as the Mvc.Server example. I have now added Satellizer to my angular app, with config like so

// satellizer
$authProvider.oauth2({
    name: 'openiddict',
    clientId: 'myClient',
    redirectUri: window.location.origin + '/signin-oidc',
    authorizationEndpoint: window.location.origin + '/connect/authorize',
    responseType: 'id_token token',
    scope: 'openid'
});

Where i register my application, in startup.cs, it looks like this

context.Applications.Add(new Application {
    Id = "myClient",
    DisplayName = "My client application",
    RedirectUri = "http://localhost:5000/signin-oidc",
    LogoutRedirectUri = "http://localhost:5000/",
    Secret = Crypto.HashPassword("secret_secret_secret"),
    Type = OpenIddictConstants.ApplicationTypes.Confidential
});

Now if i click the login button, which causes Satellizer to open a new window, my AccountController.Login method is never called??

I have added debug points to all the code, and when my new window opens, first the OpenIddictProvider.MatchEndpoint method is called. Then the ValidateClientRedirectUri method, which seems to call context.Validate();. Then nothing and the window closes....

Would it be help if i sent you a copy of my example?

context.Applications.Add(new Application {
    Id = "myClient",
    DisplayName = "My client application",
    RedirectUri = "http://localhost:5000/signin-oidc",
    LogoutRedirectUri = "http://localhost:5000/",
    Secret = Crypto.HashPassword("secret_secret_secret"),
    Type = OpenIddictConstants.ApplicationTypes.Confidential
});

Since your JS app is a public app, you can't use OpenIddictConstants.ApplicationTypes.Confidential. Instead, don't assign a secret and use OpenIddictConstants.ApplicationTypes.Public. That said, I don't think that's what's causing your issue.

Would it be help if i sent you a copy of my example?

As said on the other post, yes 😄
Upload it somewhere on GitHub and I'll give it a try.

Tried Public, just encase but that didnt fix it. Here is my example project.

Mvc.Angular.zip

I tried your app and the OpenIddict part works fine (I just had to replace the redirect_uri to match the port used by IIS). It's probably a bug in the JS part, but I can't figure out where.

Okay, it's a Satellizer bug: Popup.pollPopup immediately closes the popup if the authorization server and the JS app share the same address (which is not surprising in your case) without even checking if the current address of the popup matches the redirect_uri.

I suggest moving this bug to the Satellizer repository.

Popup.pollPopup = function() {
  var deferred = $q.defer();

  var polling = $interval(function() {
    try {
      var documentOrigin = document.location.host;
      var popupWindowOrigin = Popup.popupWindow.location.host;

      if (popupWindowOrigin === documentOrigin && (Popup.popupWindow.location.search || Popup.popupWindow.location.hash)) {
        var queryParams = Popup.popupWindow.location.search.substring(1).replace(/\/$/, '');
        var hashParams = Popup.popupWindow.location.hash.substring(1).replace(/[\/$]/, '');
        var hash = utils.parseQueryString(hashParams);
        var qs = utils.parseQueryString(queryParams);

        angular.extend(qs, hash);

        if (qs.error) {
          deferred.reject(qs);
        } else {
          deferred.resolve(qs);
        }

        $interval.cancel(polling);

        Popup.popupWindow.close();
      }
    } catch (error) {
      // Ignore DOMException: Blocked a frame with origin from accessing a cross-origin frame.
    }

    if (!Popup.popupWindow || Popup.popupWindow.closed || Popup.popupWindow.closed === undefined) {
      $interval.cancel(polling);
    }
  }, 20);

  return deferred.promise;
};

FYI, here's the Satellizer configuration I've used (after fixing the buggy Popup.pollPopup, it works fine):

$authProvider.oauth2({
    name: 'openiddict',
    clientId: 'myClient',
    redirectUri: window.location.origin + '/signin-oidc',
    authorizationEndpoint: window.location.origin + '/connect/authorize',
    responseType: 'id_token token',
    scope: ['openid'],
    requiredUrlParams: ['scope', 'nonce'],
    nonce: function() { return "Implement appropriate nonce generation"; },
    popupOptions: { width: 1028, height: 529 }
});
app.controller('AppController', ['$auth', function ($auth) {
    var that = this;

    this.login = function () {
        $auth.authenticate('openiddict').then(function (response) {
            if (response.access_token && response.id_token) {
                console.log('Access token: ' + response.access_token);
                console.log('Identity token: ' + response.id_token);

                // Todo: validate the identity token, the nonce and the access token hash.
            }
        });
    };
}]);

@PinpointTownes you said that the Satellizer code is buggy because of the redirect_uri. I believe i have fixed it, but can i just check with you that this is right, so i have the "flow" clear in my head. So when you open the window, the AccountController is called, and the user can login/register/use external login etc. Once that is done oauth redirects to the to /connect/authorize. The user is then given an access_token and redirected to /signin-oidc, which is the redirect_uri. This is when the popup window should actually close. Is that right?

In light of this, i have made this fix, Does this look ok since you completely understand how the flow works. If so, i will then submit this as a PR on the Satellizer site

Popup.pollPopup = function () {
    var deferred = $q.defer();
    var redirect_uri = null;

    var polling = $interval(function () {
        try {
            var documentOrigin = document.location.host;
            var popupWindowOrigin = Popup.popupWindow.location.host;

            if (popupWindowOrigin === documentOrigin && (Popup.popupWindow.location.search || Popup.popupWindow.location.hash)) {
                var queryParams = Popup.popupWindow.location.search.substring(1).replace(/\/$/, '');
                var hashParams = Popup.popupWindow.location.hash.substring(1).replace(/[\/$]/, '');
                var hash = utils.parseQueryString(hashParams);
                var qs = utils.parseQueryString(queryParams);

                // get the redirect_uri from the querystring here, but only get it the first time
                if (redirect_uri == null && qs.redirect_uri) {
                    redirect_uri = qs.redirect_uri;
                    return;
                }

                // got the redirect_uri, is the popup window on this uri yet?
                if (redirect_uri == Popup.popupWindow.location.href.split('#')[0]) {
                    angular.extend(qs, hash);

                    if (qs.error) {
                        deferred.reject(qs);
                    } else {
                        deferred.resolve(qs);
                    }

                    $interval.cancel(polling);

                    Popup.popupWindow.close();
                }
            }
        } catch (error) {
            // Ignore DOMException: Blocked a frame with origin from accessing a cross-origin frame.
        }

        if (!Popup.popupWindow || Popup.popupWindow.closed || Popup.popupWindow.closed === undefined) {
            $interval.cancel(polling);
        }
    }, 20);

    return deferred.promise;
};

@PinpointTownes you said that the Satellizer code is buggy because of the redirect_uri. I believe i have fixed it, but can i just check with you that this is right, so i have the "flow" clear in my head. So when you open the window, the AccountController is called, and the user can login/register/use external login etc. Once that is done oauth redirects to the to /connect/authorize. The user is then given an access_token and redirected to /signin-oidc, which is the redirect_uri. This is when the popup window should actually close. Is that right?

Yes.

Your fix seems fine, but I'm definitely not a JS expert.
FWIW, here's my quick and dirty version:

Popup.pollPopup = function (redirectUri) {
  var deferred = $q.defer();

  var polling = $interval(function() {
    try {
      var popupWindowOrigin = Popup.popupWindow.location.protocol + '//' +
                              Popup.popupWindow.location.host +
                              Popup.popupWindow.location.pathname;

      if (popupWindowOrigin === redirectUri && (Popup.popupWindow.location.search || Popup.popupWindow.location.hash)) {
        var queryParams = Popup.popupWindow.location.search.substring(1).replace(/\/$/, '');
        var hashParams = Popup.popupWindow.location.hash.substring(1).replace(/[\/$]/, '');
        var hash = utils.parseQueryString(hashParams);
        var qs = utils.parseQueryString(queryParams);

        angular.extend(qs, hash);

        if (qs.error) {
          deferred.reject(qs);
        } else {
          deferred.resolve(qs);
        }

        $interval.cancel(polling);

        Popup.popupWindow.close();
      }
    } catch (error) {
      // Ignore DOMException: Blocked a frame with origin from accessing a cross-origin frame.
    }

    if (!Popup.popupWindow || Popup.popupWindow.closed || Popup.popupWindow.closed === undefined) {
      $interval.cancel(polling);
    }
  }, 20);

  return deferred.promise;
};

@Gillardo I opened a new ticket on Satellizer's repository for you: sahat/satellizer#711.

Thanks @PinpointTownes . just want to say again how thankful i am for your help!! Btw, does IdDict support refreshTokens?

Yes, OpenIddict supports refresh tokens, even with public apps (like JS apps). But you need to use the code flow to retrieve a refresh token, it won't work with the implicit flow.

Alternatively, you can use prompt=none authorization requests by flowing the identity token in id_token_hint. It's the approach recommended by the OIDC specs. It's usually done in a hidden iframe.

Are you saying i cant use any of my existing authentication, or i just need to do something else to support refreh tokens? I dont suppose you could point me in the direction of an example, as i am not sure what i should be looking for exactly?

@PinpointTownes do you know a guide that i could look at to implement refresh tokens in openiddict? I have tried to use code flow, but upon requesting a token using a code, it doesnt return a refresh token. I suspect i am not setting up something correctly