c# - OpenIdDict and ASP.NET Core: 401 after successfully getting the token back (full repro) -
still periodically struggling openauth using openiddict (credentials flow) in asp.net core, updated latest openiddict bits , vs2017 old sample code can find @ https://github.com/myrmex/repro-oidang, full step-by-step guidance create essential startup template. hope can useful community getting started simple security scenarios, contribution simple example code welcome.
essentially followed credentials flow sample openiddict author, , can token when requesting (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 when try use token, keep getting 401, without other hint: no exception, nothing logged. request like:
get http://localhost:50728/api/values content-type: application/json authorization: bearer ...
here relevant code: first startup.cs
:
public void configureservices(iservicecollection services) { // setup options di // https://docs.asp.net/en/latest/fundamentals/configuration.html services.addoptions(); // cors (note: if using azure, remember enable cors in portal, too!) services.addcors(); // add entity framework , context(s) using in-memory // (or use commented line use connection string real db) services.addentityframeworksqlserver() .adddbcontext<applicationdbcontext>(options => { // options.usesqlserver(configuration.getconnectionstring("authentication"))); options.useinmemorydatabase(); // register entity sets needed openiddict. // note: use generic overload if need // replace default openiddict entities. options.useopeniddict(); }); // register identity services services.addidentity<applicationuser, identityrole>() .addentityframeworkstores<applicationdbcontext>() .adddefaulttokenproviders(); // configure identity use same jwt claims openiddict instead // of legacy ws-federation claims uses default (claimtypes), // saves doing mapping in 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 openiddict services services.addopeniddict(options => { // register entity framework stores options.addentityframeworkcorestores<applicationdbcontext>(); // register asp.net core mvc binder used openiddict. // note: if don't call method, won't able // bind openidconnectrequest or openidconnectresponse parameters // action methods. alternatively, can still use lower-level // httpcontext.getopenidconnectrequest() api. options.addmvcbinders(); // enable 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 password flow options.allowpasswordflow(); options.allowrefreshtokenflow(); // during development, can disable https requirement options.disablehttpsrequirement(); // note: use jwt access tokens instead of default // encrypted format, following lines required: // options.usejsonwebtokens(); // options.addephemeralsigningkey(); }); // add framework services services.addmvc() .addjsonoptions(options => { options.serializersettings.contractresolver = new newtonsoft.json.serialization.camelcasepropertynamescontractresolver(); }); // seed database demo user details services.addtransient<idatabaseinitializer, databaseinitializer>(); // swagger services.addswaggergen(); } // method gets called runtime. use method configure http request pipeline. public void configure(iapplicationbuilder app, ihostingenvironment env, iloggerfactory loggerfactory, idatabaseinitializer databaseinitializer) { loggerfactory.addconsole(configuration.getsection("logging")); loggerfactory.adddebug(); loggerfactory.addnlog(); // https://docs.microsoft.com/en-us/aspnet/core/fundamentals/error-handling if (env.isdevelopment()) app.usedeveloperexceptionpage(); // serve 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 middleware used validate access tokens , protect api endpoints app.useoauthvalidation(); app.useopeniddict(); app.usemvc(); // app.usemvcwithdefaultroute(); // app.usewelcomepage(); // seed database databaseinitializer.seed().getawaiter().getresult(); // swagger // enable middleware serve generated swagger json endpoint app.useswagger(); // enable middleware serve swagger-ui assets (html, js, css etc.) app.useswaggerui(); }
and controller (you can find whole solution in 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 new claimsprincipal containing claims // used create id_token, token or code. claimsprincipal principal = await _signinmanager.createuserprincipalasync(user); // create new authentication ticket holding user identity. authenticationticket ticket = new authenticationticket( principal, new authenticationproperties(), openidconnectserverdefaults.authenticationscheme); // set list of scopes granted client application. // note: offline_access scope must granted // allow openiddict return 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: default, claims not automatically included in access , identity tokens. // allow openiddict serialize them, must attach them destination, specifies // whether should included in access tokens, in identity tokens or in both. foreach (var claim in ticket.principal.claims) { // never include security stamp in access , identity tokens, it's secret value. if (claim.type == _identityoptions.value.claimsidentity.securitystampclaimtype) continue; list<string> destinations = new list<string> { openidconnectconstants.destinations.accesstoken }; // add iterated claim id_token if corresponding scope granted client application. // other claims added access_token, encrypted when using 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 prefer not bind request parameter, can still use: // openidconnectrequest request = httpcontext.getopenidconnectrequest(); debug.assert(request.istokenrequest(), "the openiddict binder asp.net core mvc not registered. " + "make sure services.addopeniddict().addmvcbinders() correctly called."); if (!request.ispasswordgranttype()) { return badrequest(new openidconnectresponse { error = openidconnectconstants.errors.unsupportedgranttype, errordescription = "the specified grant type 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 invalid." }); } // ensure user allowed sign in. if (!await _signinmanager.cansigninasync(user)) { return badrequest(new openidconnectresponse { error = openidconnectconstants.errors.invalidgrant, errordescription = "the specified user not allowed sign in." }); } // reject token request if two-factor authentication has been enabled user. if (_usermanager.supportsusertwofactor && await _usermanager.gettwofactorenabledasync(user)) { return badrequest(new openidconnectresponse { error = openidconnectconstants.errors.invalidgrant, errordescription = "the specified user not allowed sign in." }); } // ensure user not locked out. if (_usermanager.supportsuserlockout && await _usermanager.islockedoutasync(user)) { return badrequest(new openidconnectresponse { error = openidconnectconstants.errors.invalidgrant, errordescription = "the username/password couple invalid." }); } // ensure password 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 invalid." }); } if (_usermanager.supportsuserlockout) await _usermanager.resetaccessfailedcountasync(user); // create 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 authorization request asp.net environment. openidconnectrequest request = httpcontext.getopenidconnectrequest(); // ask asp.net core identity delete local , external cookies created // when user agent redirected external identity provider // after successful authentication flow (e.g google or facebook). await _signinmanager.signoutasync(); // returning signoutresult ask openiddict redirect user agent // post_logout_redirect_uri specified 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); // simplify, in demo have 1 role 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 blog post, token format used openiddict changed recently, makes tokens issued latest openiddict bits incompatible old oauth2 validation middleware version you're using.
migrate aspnet.security.oauth.validation
1.0.0
, should work.
Comments
Post a Comment