spring mvc - Basic auth must be preemptive with protobuf converter -


as explained in this blog post can use protobufhttpmessageconverter serialize/deserialize protobuf messages. when using basic authentication, if choose json media type protobuf message converter can use preemptive auth or not, doesn't matter. however, if choose protobuf media type must use preemptive authentication otherwise not work, i.e., server returns unauthorized response expected basic auth response not seem processed. when switch on preemptive authentication, i.e., basic auth response sent right away, works expected. nonetheless, seems rather strange me. know why?

you'll find below sample code reproduces issue. access web service using rest client instance.

@springbootapplication public class demoapplication {      public static void main(string[] args) {         springapplication.run(demoapplication.class, args);     }      @bean     protobufhttpmessageconverter protobufhttpmessageconverter() {         return new protobufhttpmessageconverter();     } }  @configuration @enablewebsecurity class securityconfig extends websecurityconfigureradapter {      @autowired     public void configureglobal(authenticationmanagerbuilder auth)             throws exception {         auth.inmemoryauthentication()                 .withuser("test").password("test").roles("user");     }      @override     protected void configure(httpsecurity http) throws exception {         http.authorizerequests()                 .antmatchers("/**").hasrole("user")                 .and()                 .httpbasic()                 .and()                 .sessionmanagement().sessioncreationpolicy(                         sessioncreationpolicy.stateless)                 .and()                 .csrf().disable();     } }  @restcontroller class customerrestcontroller {      @requestmapping("/")     data.customer customer() {         return customer(5, "toto");     }      private data.customer customer(int id, string f) {         return data.customer.newbuilder()                 .setid(id)                 .setname(f)                 .build();     } } 

and protobuf message data.proto:

package demo;  message customer {     optional int32 id = 1;     optional string name = 2; } 

here's logs when not using preemptive auth following request header: "accept:application/x-protobuf". can notice after unauthorized response nothing happens (the basic auth response should processed).

o.s.security.web.filterchainproxy        : / @ position 1 of 11 in additional filter chain; firing filter: 'webasyncmanagerintegrationfilter' o.s.security.web.filterchainproxy        : / @ position 2 of 11 in additional filter chain; firing filter: 'securitycontextpersistencefilter' o.s.security.web.filterchainproxy        : / @ position 3 of 11 in additional filter chain; firing filter: 'headerwriterfilter' o.s.s.w.header.writers.hstsheaderwriter  : not injecting hsts header since did not match requestmatcher org.springframework.security.web.header.writers.hstsheaderwriter$securerequestmatcher@5576e87d o.s.security.web.filterchainproxy        : / @ position 4 of 11 in additional filter chain; firing filter: 'logoutfilter' o.s.s.w.u.matcher.antpathrequestmatcher  : checking match of request : '/'; against '/logout' o.s.security.web.filterchainproxy        : / @ position 5 of 11 in additional filter chain; firing filter: 'basicauthenticationfilter' o.s.security.web.filterchainproxy        : / @ position 6 of 11 in additional filter chain; firing filter: 'requestcacheawarefilter' o.s.security.web.filterchainproxy        : / @ position 7 of 11 in additional filter chain; firing filter: 'securitycontextholderawarerequestfilter' o.s.security.web.filterchainproxy        : / @ position 8 of 11 in additional filter chain; firing filter: 'anonymousauthenticationfilter' o.s.s.w.a.anonymousauthenticationfilter  : populated securitycontextholder anonymous token: 'org.springframework.security.authentication.anonymousauthenticationtoken@9055e4a6: principal: anonymoususer; credentials: [protected]; authenticated: true; details: org.springframework.security.web.authentication.webauthenticationdetails@957e: remoteipaddress: 127.0.0.1; sessionid: null; granted authorities: role_anonymous' o.s.security.web.filterchainproxy        : / @ position 9 of 11 in additional filter chain; firing filter: 'sessionmanagementfilter' o.s.security.web.filterchainproxy        : / @ position 10 of 11 in additional filter chain; firing filter: 'exceptiontranslationfilter' o.s.security.web.filterchainproxy        : / @ position 11 of 11 in additional filter chain; firing filter: 'filtersecurityinterceptor' o.s.s.w.u.matcher.antpathrequestmatcher  : request '/' matched universal pattern '/**' o.s.s.w.a.i.filtersecurityinterceptor    : secure object: filterinvocation: url: /; attributes: [hasrole('role_user')] o.s.s.w.a.i.filtersecurityinterceptor    : authenticated: org.springframework.security.authentication.anonymousauthenticationtoken@9055e4a6: principal: anonymoususer; credentials: [protected]; authenticated: true; details: org.springframework.security.web.authentication.webauthenticationdetails@957e: remoteipaddress: 127.0.0.1; sessionid: null; granted authorities: role_anonymous o.s.s.access.vote.affirmativebased       : voter: org.springframework.security.web.access.expression.webexpressionvoter@79376d4e, returned: -1 o.s.s.w.a.exceptiontranslationfilter     : access denied (user anonymous); redirecting authentication entry point o.s.s.w.a.exceptiontranslationfilter     : calling authentication entry point. s.w.a.delegatingauthenticationentrypoint : trying match using requestheaderrequestmatcher [expectedheadername=x-requested-with, expectedheadervalue=xmlhttprequest] s.w.a.delegatingauthenticationentrypoint : no match found. using default entry point org.springframework.security.web.authentication.www.basicauthenticationentrypoint@4e65575 o.s.web.servlet.dispatcherservlet        : dispatcherservlet name 'dispatcherservlet' processing request [/error] s.w.s.m.m.a.requestmappinghandlermapping : looking handler method path /error s.w.s.m.m.a.requestmappinghandlermapping : returning handler method [public org.springframework.http.responseentity<java.util.map<java.lang.string, java.lang.object>> org.springframework.boot.autoconfigure.web.basicerrorcontroller.error(javax.servlet.http.httpservletrequest)] o.s.b.f.s.defaultlistablebeanfactory     : returning cached instance of singleton bean 'basicerrorcontroller' o.s.web.servlet.dispatcherservlet        : last-modified value [/error] is: -1 .m.m.a.exceptionhandlerexceptionresolver : resolving exception handler [public org.springframework.http.responseentity<java.util.map<java.lang.string, java.lang.object>> org.springframework.boot.autoconfigure.web.basicerrorcontroller.error(javax.servlet.http.httpservletrequest)]: org.springframework.web.httpmediatypenotacceptableexception: not find acceptable representation .w.s.m.a.responsestatusexceptionresolver : resolving exception handler [public org.springframework.http.responseentity<java.util.map<java.lang.string, java.lang.object>> org.springframework.boot.autoconfigure.web.basicerrorcontroller.error(javax.servlet.http.httpservletrequest)]: org.springframework.web.httpmediatypenotacceptableexception: not find acceptable representation .w.s.m.s.defaulthandlerexceptionresolver : resolving exception handler [public org.springframework.http.responseentity<java.util.map<java.lang.string, java.lang.object>> org.springframework.boot.autoconfigure.web.basicerrorcontroller.error(javax.servlet.http.httpservletrequest)]: org.springframework.web.httpmediatypenotacceptableexception: not find acceptable representation o.s.web.servlet.dispatcherservlet        : null modelandview returned dispatcherservlet name 'dispatcherservlet': assuming handleradapter completed request handling o.s.web.servlet.dispatcherservlet        : completed request s.s.w.c.securitycontextpersistencefilter : securitycontextholder cleared, request processing completed 

you can compare following logs come request without preemptive auth following request header: "accept:application/json". can notice after unauthorized response, auth response processed server , expected json representation returned.

o.s.security.web.filterchainproxy        : / @ position 1 of 11 in additional filter chain; firing filter: 'webasyncmanagerintegrationfilter' o.s.security.web.filterchainproxy        : / @ position 2 of 11 in additional filter chain; firing filter: 'securitycontextpersistencefilter' o.s.security.web.filterchainproxy        : / @ position 3 of 11 in additional filter chain; firing filter: 'headerwriterfilter' o.s.s.w.header.writers.hstsheaderwriter  : not injecting hsts header since did not match requestmatcher org.springframework.security.web.header.writers.hstsheaderwriter$securerequestmatcher@5576e87d o.s.security.web.filterchainproxy        : / @ position 4 of 11 in additional filter chain; firing filter: 'logoutfilter' o.s.s.w.u.matcher.antpathrequestmatcher  : checking match of request : '/'; against '/logout' o.s.security.web.filterchainproxy        : / @ position 5 of 11 in additional filter chain; firing filter: 'basicauthenticationfilter' o.s.security.web.filterchainproxy        : / @ position 6 of 11 in additional filter chain; firing filter: 'requestcacheawarefilter' o.s.security.web.filterchainproxy        : / @ position 7 of 11 in additional filter chain; firing filter: 'securitycontextholderawarerequestfilter' o.s.security.web.filterchainproxy        : / @ position 8 of 11 in additional filter chain; firing filter: 'anonymousauthenticationfilter' o.s.s.w.a.anonymousauthenticationfilter  : populated securitycontextholder anonymous token: 'org.springframework.security.authentication.anonymousauthenticationtoken@9055e4a6: principal: anonymoususer; credentials: [protected]; authenticated: true; details: org.springframework.security.web.authentication.webauthenticationdetails@957e: remoteipaddress: 127.0.0.1; sessionid: null; granted authorities: role_anonymous' o.s.security.web.filterchainproxy        : / @ position 9 of 11 in additional filter chain; firing filter: 'sessionmanagementfilter' o.s.security.web.filterchainproxy        : / @ position 10 of 11 in additional filter chain; firing filter: 'exceptiontranslationfilter' o.s.security.web.filterchainproxy        : / @ position 11 of 11 in additional filter chain; firing filter: 'filtersecurityinterceptor' o.s.s.w.u.matcher.antpathrequestmatcher  : request '/' matched universal pattern '/**' o.s.s.w.a.i.filtersecurityinterceptor    : secure object: filterinvocation: url: /; attributes: [hasrole('role_user')] o.s.s.w.a.i.filtersecurityinterceptor    : authenticated: org.springframework.security.authentication.anonymousauthenticationtoken@9055e4a6: principal: anonymoususer; credentials: [protected]; authenticated: true; details: org.springframework.security.web.authentication.webauthenticationdetails@957e: remoteipaddress: 127.0.0.1; sessionid: null; granted authorities: role_anonymous o.s.s.access.vote.affirmativebased       : voter: org.springframework.security.web.access.expression.webexpressionvoter@79376d4e, returned: -1 o.s.s.w.a.exceptiontranslationfilter     : access denied (user anonymous); redirecting authentication entry point o.s.s.w.a.exceptiontranslationfilter     : calling authentication entry point. s.w.a.delegatingauthenticationentrypoint : trying match using requestheaderrequestmatcher [expectedheadername=x-requested-with, expectedheadervalue=xmlhttprequest] s.w.a.delegatingauthenticationentrypoint : no match found. using default entry point org.springframework.security.web.authentication.www.basicauthenticationentrypoint@4e65575 o.s.web.servlet.dispatcherservlet        : dispatcherservlet name 'dispatcherservlet' processing request [/error] s.w.s.m.m.a.requestmappinghandlermapping : looking handler method path /error s.w.s.m.m.a.requestmappinghandlermapping : returning handler method [public org.springframework.http.responseentity<java.util.map<java.lang.string, java.lang.object>> org.springframework.boot.autoconfigure.web.basicerrorcontroller.error(javax.servlet.http.httpservletrequest)] o.s.b.f.s.defaultlistablebeanfactory     : returning cached instance of singleton bean 'basicerrorcontroller' o.s.web.servlet.dispatcherservlet        : last-modified value [/error] is: -1 o.s.w.s.m.m.a.httpentitymethodprocessor  : written [{timestamp=tue apr 14 14:41:15 cest 2015, status=401, error=unauthorized, message=full authentication required access resource, path=/}] "application/json;charset=utf-8" using [org.springframework.http.converter.json.mappingjackson2httpmessageconverter@5d603063] o.s.web.servlet.dispatcherservlet        : null modelandview returned dispatcherservlet name 'dispatcherservlet': assuming handleradapter completed request handling o.s.web.servlet.dispatcherservlet        : completed request s.s.w.c.securitycontextpersistencefilter : securitycontextholder cleared, request processing completed o.s.security.web.filterchainproxy        : / @ position 1 of 11 in additional filter chain; firing filter: 'webasyncmanagerintegrationfilter' o.s.security.web.filterchainproxy        : / @ position 2 of 11 in additional filter chain; firing filter: 'securitycontextpersistencefilter' o.s.security.web.filterchainproxy        : / @ position 3 of 11 in additional filter chain; firing filter: 'headerwriterfilter' o.s.s.w.header.writers.hstsheaderwriter  : not injecting hsts header since did not match requestmatcher org.springframework.security.web.header.writers.hstsheaderwriter$securerequestmatcher@5576e87d o.s.security.web.filterchainproxy        : / @ position 4 of 11 in additional filter chain; firing filter: 'logoutfilter' o.s.s.w.u.matcher.antpathrequestmatcher  : checking match of request : '/'; against '/logout' o.s.security.web.filterchainproxy        : / @ position 5 of 11 in additional filter chain; firing filter: 'basicauthenticationfilter' o.s.s.w.a.www.basicauthenticationfilter  : basic authentication authorization header found user 'test' o.s.s.authentication.providermanager     : authentication attempt using org.springframework.security.authentication.dao.daoauthenticationprovider o.s.b.f.s.defaultlistablebeanfactory     : returning cached instance of singleton bean 'delegatingapplicationlistener' o.s.s.w.a.www.basicauthenticationfilter  : authentication success: org.springframework.security.authentication.usernamepasswordauthenticationtoken@442bd3dc: principal: org.springframework.security.core.userdetails.user@364492: username: test; password: [protected]; enabled: true; accountnonexpired: true; credentialsnonexpired: true; accountnonlocked: true; granted authorities: role_user; credentials: [protected]; authenticated: true; details: org.springframework.security.web.authentication.webauthenticationdetails@957e: remoteipaddress: 127.0.0.1; sessionid: null; granted authorities: role_user o.s.security.web.filterchainproxy        : / @ position 6 of 11 in additional filter chain; firing filter: 'requestcacheawarefilter' o.s.security.web.filterchainproxy        : / @ position 7 of 11 in additional filter chain; firing filter: 'securitycontextholderawarerequestfilter' o.s.security.web.filterchainproxy        : / @ position 8 of 11 in additional filter chain; firing filter: 'anonymousauthenticationfilter' o.s.s.w.a.anonymousauthenticationfilter  : securitycontextholder not populated anonymous token, contained: 'org.springframework.security.authentication.usernamepasswordauthenticationtoken@442bd3dc: principal: org.springframework.security.core.userdetails.user@364492: username: test; password: [protected]; enabled: true; accountnonexpired: true; credentialsnonexpired: true; accountnonlocked: true; granted authorities: role_user; credentials: [protected]; authenticated: true; details: org.springframework.security.web.authentication.webauthenticationdetails@957e: remoteipaddress: 127.0.0.1; sessionid: null; granted authorities: role_user' o.s.security.web.filterchainproxy        : / @ position 9 of 11 in additional filter chain; firing filter: 'sessionmanagementfilter' s.compositesessionauthenticationstrategy : delegating org.springframework.security.web.authentication.session.changesessionidauthenticationstrategy@6c2f0571 o.s.security.web.filterchainproxy        : / @ position 10 of 11 in additional filter chain; firing filter: 'exceptiontranslationfilter' o.s.security.web.filterchainproxy        : / @ position 11 of 11 in additional filter chain; firing filter: 'filtersecurityinterceptor' o.s.s.w.u.matcher.antpathrequestmatcher  : request '/' matched universal pattern '/**' o.s.s.w.a.i.filtersecurityinterceptor    : secure object: filterinvocation: url: /; attributes: [hasrole('role_user')] o.s.s.w.a.i.filtersecurityinterceptor    : authenticated: org.springframework.security.authentication.usernamepasswordauthenticationtoken@442bd3dc: principal: org.springframework.security.core.userdetails.user@364492: username: test; password: [protected]; enabled: true; accountnonexpired: true; credentialsnonexpired: true; accountnonlocked: true; granted authorities: role_user; credentials: [protected]; authenticated: true; details: org.springframework.security.web.authentication.webauthenticationdetails@957e: remoteipaddress: 127.0.0.1; sessionid: null; granted authorities: role_user o.s.s.access.vote.affirmativebased       : voter: org.springframework.security.web.access.expression.webexpressionvoter@79376d4e, returned: 1 o.s.s.w.a.i.filtersecurityinterceptor    : authorization successful o.s.s.w.a.i.filtersecurityinterceptor    : runasmanager did not change authentication object o.s.security.web.filterchainproxy        : / reached end of additional filter chain; proceeding original chain o.s.web.servlet.dispatcherservlet        : dispatcherservlet name 'dispatcherservlet' processing request [/] s.w.s.m.m.a.requestmappinghandlermapping : looking handler method path / s.w.s.m.m.a.requestmappinghandlermapping : returning handler method [demo.data$customer demo.customerrestcontroller.customer()] o.s.b.f.s.defaultlistablebeanfactory     : returning cached instance of singleton bean 'customerrestcontroller' o.s.web.servlet.dispatcherservlet        : last-modified value [/] is: -1 m.m.a.requestresponsebodymethodprocessor : written [id: 5, name: "toto"] "application/json;charset=utf-8" using [org.springframework.http.converter.protobuf.protobufhttpmessageconverter@42d2b7d8] o.s.web.servlet.dispatcherservlet        : null modelandview returned dispatcherservlet name 'dispatcherservlet': assuming handleradapter completed request handling o.s.web.servlet.dispatcherservlet        : completed request o.s.s.w.a.exceptiontranslationfilter     : chain processed s.s.w.c.securitycontextpersistencefilter : securitycontextholder cleared, request processing completed 

this appears issue in spring boot. created spring-projects/spring-boot/issues#2827.

what happening?

this happens because basicerrorcontroller creating responseentity<map<string, object>>. when protobufhttpmessageconverter attempts write body, cannot because protobufhttpmessageconverter only supports writing protobuf message objects.

working around issue

the issue can handled creating custom controller process errors. example:

package demo;  option java_package = "demo"; option java_outer_classname = "data";  message mapfieldentry {     required string key = 1;     required string value = 2; }  message error {     repeated mapfieldentry errors = 1; } 

generate respective java classes. create controller:

package demo;  import java.util.map;  import javax.servlet.http.httpservletrequest;  import org.springframework.beans.factory.annotation.autowired; import org.springframework.boot.autoconfigure.web.errorattributes; import org.springframework.http.httpstatus; import org.springframework.http.responseentity; import org.springframework.stereotype.controller; import org.springframework.web.bind.annotation.requestmapping; import org.springframework.web.bind.annotation.responsebody; import org.springframework.web.context.request.requestattributes; import org.springframework.web.context.request.servletrequestattributes;  import demo.data.error.builder; import demo.data.mapfieldentry;  /**  * @author rob winch  */ @controller public class errorcontroller {     private final errorattributes errorattributes;      @autowired     public errorcontroller(errorattributes errorattributes) {         this.errorattributes = errorattributes;     }      @requestmapping(value = "/error", produces = "application/x-protobuf")     @responsebody     public responseentity<data.error> error(httpservletrequest request) {         map<string, object> body = geterrorattributes(request, gettraceparameter(request));          builder errorsbuilder = data.error.newbuilder();         for(map.entry<string, object> error : body.entryset()) {             demo.data.mapfieldentry.builder entrybuilder = mapfieldentry                     .newbuilder()                     .setkey(error.getkey())                     .setvalue(string.valueof(error.getvalue()));             errorsbuilder.adderrors(entrybuilder.build());         }         data.error errors = errorsbuilder.build();         httpstatus status = getstatus(request);         return new responseentity<data.error>(errors, status);     }      private boolean gettraceparameter(httpservletrequest request) {         string parameter = request.getparameter("trace");         if (parameter == null) {             return false;         }         return !"false".equals(parameter.tolowercase());     }      private map<string, object> geterrorattributes(httpservletrequest request,             boolean includestacktrace) {         requestattributes requestattributes = new servletrequestattributes(request);         return this.errorattributes.geterrorattributes(requestattributes,                 includestacktrace);     }      private httpstatus getstatus(httpservletrequest request) {         integer statuscode = (integer) request                 .getattribute("javax.servlet.error.status_code");         if (statuscode != null) {             try {                 return httpstatus.valueof(statuscode);             }             catch (exception ex) {             }         }         return httpstatus.internal_server_error;     } } 

Popular posts from this blog