I have a rest API implemented in Java (MSF4J codegen from swagger) and a swagger 2 definition that describes it.
A swagger UI is hosted on a web server. The API is deployed on a VM somewhere on the internet.
My Problem is that the "try it out" function of the swagger UI doesn't work. I always get a "401 Unauthorized". When I take the curl command from the UI and paste it into my terminal it works.
Last week I didn't have HTTPS or Basic Authentication - just HTTP - and it worked fine. Now I don't know why it doesn't work.
Since I changed the swagger definition to https the UI makes an OPTIONS request. I implemented that, but I get 401 responses.
The certificate comes from Lets Encrypt and is used by an apache web server. The apache is a proxy to the rest api on the same machine.
Here is my authentication interceptor:
public class BasicAuthSecurityInterceptor extends AbstractBasicAuthSecurityInterceptor {
#Override
protected boolean authenticate(String username, String password) {
if (checkCredentials(username, password))
return true;
return false;
}
private boolean checkCredentials(String username, String password) {
if (username.equals("testuser"))
return BCrypt.checkpw(password, "$2a$10$iXRsLgkJg3ZZGy4utrdNyunHcamiL2RmrKHKyJAoV4kHVGhFv.d6G");
return false;
}
}
Here is a part of the api:
public abstract class DeviceApiService {
private static final Logger LOGGER = LogManager.getLogger();
public abstract Response deviceGet() throws NotFoundException;
public abstract Response deviceIdAvailableLoadGet(Integer id, Long from, Long to, String resolution)
throws NotFoundException;
public abstract Response deviceIdGet(Integer id) throws NotFoundException;
protected Response getOptionsResponse() {
String allowedOrigin = "";
try {
allowedOrigin = PropertyFileHandler.getInstance().getPropertyValueFromKey("api.cors.allowed");
} catch (IllegalArgumentException | PropertyException | IOException e) {
LOGGER.error("Could not get allowed origin.", e);
}
Response response = Response.ok().header("Allow", "GET").header("Access-Control-Allow-Origin", allowedOrigin)
.header("Access-Control-Allow-Headers", "authorization, content-type").build();
return response;
}
}
public class DeviceApi {
private final DeviceApiService delegate = DeviceApiServiceFactory.getDeviceApi();
// #formatter:off
#GET
#Produces({ "application/json" })
#io.swagger.annotations.ApiOperation(
value = "Get devices",
notes = "",
response = Device.class,
responseContainer = "List",
authorizations = { #io.swagger.annotations.Authorization(value = "basicAuth") },
tags = { "Device", }
)
#io.swagger.annotations.ApiResponses(
value = { #io.swagger.annotations.ApiResponse(
code = 200,
message = "200 OK",
response = Device.class,
responseContainer = "List")
})
public Response deviceGet() throws NotFoundException {
return delegate.deviceGet();
}
#OPTIONS
#Consumes({ "application/json" })
#Produces({ "application/json" })
#io.swagger.annotations.ApiOperation(value = "CORS support", notes = "", response = Void.class, authorizations = {
#io.swagger.annotations.Authorization(value = "basicAuth") }, tags = { "Device", })
#io.swagger.annotations.ApiResponses(value = {
#io.swagger.annotations.ApiResponse(code = 200, message = "Default response for CORS method", response = Void.class) })
public Response deviceOptions() throws NotFoundException {
return delegate.getOptionsResponse();
}
}
EDIT:
This are the headers of the request the swagger ui creates:
Accept: text/html,application/xhtml+xm…plication/xml;q=0.9,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: de,en-US;q=0.7,en;q=0.3
Access-Control-Request-Headers: authorization
Access-Control-Request-Method: GET
Connection: keep-alive
DNT: 1
Host: api.myfancyurl.com
Origin: http://apidoc.myfancyurl.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; …) Gecko/20100101 Firefox/61.0
It seems that the authorization header is missing. When I edit the request and resend it with the authorization header and encoded credentials it works.
But I don't know why swagger doesn't add this header. Should one accept all options requests without authorization?
Related
I'm trying to get a value from my rest api but for some reason it not letting me get it from an authorize user but when i set it permitAll() it does. I'm receiving a 403 error code.
Backend
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.authorizeRequests().antMatchers(HttpMethod.GET,"/token/refresh/**").hasAnyAuthority("ROLE_USER");
http.authorizeRequests().antMatchers(HttpMethod.GET,"/api/user/**").hasAnyAuthority("ROLE_USER");
http.authorizeRequests().antMatchers(HttpMethod.GET,"/api/user/{username}/**").hasAnyAuthority("[ROLE_ADMIN]");
http.authorizeRequests().antMatchers(HttpMethod.POST,"/api/user/save/**").hasAnyAuthority("ROLE_ADMIN");
http.authorizeRequests().antMatchers("/api/login/**","/api/user/save/**","/api/role/addtouser/**").permitAll();
http.authorizeRequests().anyRequest().authenticated();
http.addFilter(customAuthenticationFilter);
http.addFilterBefore(new CustomAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class);
http.httpBasic();
Front-End
here is the request is being send.
getUserId(username) {
this.http.get(`http://localhost:8080/api/user/${username}`).subscribe(
resp=>{
sessionStorage.setItem("id",resp.toString())
}
)
}
Interceptor
this is my interceptor i think it's adding it to the header
export class HttpInterceptorInterceptor implements HttpInterceptor {
constructor(private authrnticationService: AuthenticationServiceService, private router:Router) {}
intercept(req: HttpRequest<any>, next: HttpHandler){
let httpHeaders = new HttpHeaders();
let basicAuthHeaderString=this.authrnticationService.getAuthenticatedtoken();
let username= this.authrnticationService.getEmail();
if (!req.headers.has('Content-Type')) {
httpHeaders = httpHeaders.append('Content-Type', 'application/x-www-form-urlencoded')
}
if (basicAuthHeaderString) {
httpHeaders = httpHeaders.append('Authorization', basicAuthHeaderString)
}
const xhr=req.clone({
headers: httpHeaders
})
return next.handle(xhr).pipe(tap(() => {},
(err: any) => {
if (err instanceof HttpErrorResponse) {
if (err.status !== 401) {
return;
}
sessionStorage.clear()
this.router.navigate(['login']);
}
}));
}
}
I've been debugging this for three hours, I still cannot explain why my custom headers (registered via a client request filter) are not sent.
The client is configured as such (full source here):
private WebTarget webTarget(String host, String appId, String appKey) {
return newClient(clientConfiguration(appId, appKey))
.target(host + "/rest");
}
private Configuration clientConfiguration(String appId, String appKey) {
ClientConfig config = new ClientConfig();
config.register(requestFilter(appId, appKey));
return config;
}
private ClientRequestFilter requestFilter(String appId, String appKey) {
return new VidalRequestFilter(apiCredentials(appId, appKey));
}
The filter is as follows:
public class VidalRequestFilter implements ClientRequestFilter {
private final ApiCredentials credentials;
public VidalRequestFilter(ApiCredentials credentials) {
this.credentials = credentials;
}
#Override
public void filter(ClientRequestContext requestContext) throws IOException {
MultivaluedMap<String, Object> headers = requestContext.getHeaders();
headers.add(ACCEPT, APPLICATION_ATOM_XML_TYPE);
headers.add("app_id", credentials.getApplicationId());
headers.add("app_key", credentials.getApplicationKey());
}
}
And the call is like:
String response = webTarget
.path("api/packages")
.request()
.get()
.readEntity(String.class);
All I get is 403 forbidden, because the specific endpoint I am calling is protected (the auth is performed with the custom headers defined above).
The weirdest thing is that, while I'm debugging, I see that sun.net.www.MessageHeader is properly invoked during the request write (i.e. the instance is valued as such: sun.net.www.MessageHeader#14f9390f7 pairs: {GET /rest/api/packages HTTP/1.1: null}{Accept: application/atom+xml}{app_id: XXX}{app_key: YYY}{User-Agent: Jersey/2.22.1 (HttpUrlConnection 1.8.0_45)}{Host: ZZZ}{Connection: keep-alive}.
However, I have the confirmation that neither our API server, nor its reverse proxy received GET requests with the required auth headers (a first HEAD request seems to be OK, though).
I know for sure the credentials are good 'cause the equivalent curl command just works!
I tried the straightforward approach to set headers directly when defining the call without any success.
What am I missing?
I have a REST endpoint #POST where the form params are null when the Content-Type is application/x-www-form-urlencoded. There is a ContainerRequestFilter earlier in the chain (code at the bottom) that takes the request, changes the stream to a BufferedInputStream, and then logs the request. If I remove this logging code, the endpoint has the correct form params. Otherwise, they're null and I can't figure out why.
Now if I use application/json, my endpoint has the correct params regardless if the logger is enabled or disabled.
I need application/x-www-form-urlencoded because the REST endpoint needs to redirect and browsers prevent redirection if the request isn't standard (preflight)
REST Endpoint that isn't working (OAuthRequest has null members)
#Stateless
#Path("v1/oauth2")
#Consumes(MediaType.APPLICATION_FORM_URLENCODED)
#Produces(MediaType.APPLICATION_JSON)
public class OAuthTokenResource {
#POST
public Response getToken(#Form OAuthRequest oauthRequest) {
...
}
OAuthRequest
public class OAuthRequest {
#FormParam(OAuthParam.CLIENT_ID)
#JsonProperty(OAuthParam.CLIENT_ID)
private String clientId;
#URL
#FormParam(OAuthParam.REDIRECT_URI)
#JsonProperty(OAuthParam.REDIRECT_URI)
private String redirectUri;
#FormParam(OAuthParam.USERNAME)
private String username;
#FormParam(OAuthParam.PASSWORD)
private String password;
...
}
Logging Filter
#Override
public void filter(final ContainerRequestContext context) throws IOException {
...
if (logEntity && context.hasEntity()) {
context.setEntityStream(logInboundEntity(builder, context.getEntityStream(), context.getMediaType()));
}
logger.debug(builder.toString());
}
private InputStream logInboundEntity(final StringBuilder builder, InputStream stream, MediaType mediaType) throws IOException {
if (!stream.markSupported()) {
stream = new BufferedInputStream(stream);
}
stream.mark(maxEntitySize + 1);
final byte[] entity = new byte[maxEntitySize + 1];
final int entitySize = stream.read(entity);
if ( entitySize > 0 ) {
String body = new String(entity, 0, Math.min(entitySize, maxEntitySize), StandardCharsets.UTF_8);
builder.append("\nBody: ");
builder.append(body);
}
if (entitySize > maxEntitySize) {
builder.append(MORE_INDICATOR);
}
stream.reset();
return stream;
}
Okay I am still not sure why #Form and #FormParam does not work if the InputStream is read during the filter chain.
But, I discovered a workaround as follows.
#POST
public Response getToken(MultivaluedMap<String, String> formParams) {
...
}
This provides the same behavior as during application/json as the params are already set even if the InputStream has been consumed.
However ultimately we went with disabling logging of the request body in our filter for security reasons.
I'm using robospice for my RESTful Communication to a backend.
Here is the specification of the interface:
## carid [/cars]
### Register a car[POST]
+ Request (application/json)
+ Header
Accepted-Language: en
Authorization: Basic xyz
+ Body
{
"carId" : "ad885f2b-dbd3-49ad-aa34-a7671cf5e337"
}
+ Response 200 (application/json)
{
"magicQuestionDefined": true,
"newCar": true
}
+ Response 401
+ Response 404 (application/json)
{
"message" : "No car owner found"
}
+ Response 400
+ Response 500
Here ist the snipped of my listener, when I receive an error (in my case its only a problem with errors, the normal case returning a json works fine) :
private class RegisterCarRequestListener implements RequestListener<RegisterCarResult> {
#Override
public void onRequestFailure(SpiceException e) {
// e: Networkexception
// e.cause=RestClientException
// e.cause.detailMessage="Could not extract response: no suitable HttpMessageConverter found for response type [ch.mycarapp.lib.model.RegisterCarResult] and content type [text/html;charset=iso-8859-1]"
}
It all works fine until the Backend answers with a 401. In this case I except an Exception of type:
HttpStatusCodeException for evaluating the exception status-code.
The reason is "e.cause.detailMessage="Could not extract response: no suitable HttpMessageConverter found for response type [ch.mycarapp.lib.model.RegisterCarResult] and content type [text/html;charset=iso-8859-1]" but it is just a status code 401 that gets returned!
The service is created like this:
public class JsonCarService extends SpringAndroidSpiceService {
#Override
public CacheManager createCacheManager(Application application) throws CacheCreationException {
final CacheManager cacheManager = new CacheManager();
final JacksonObjectPersisterFactory jacksonObjectPersisterFactory = new JacksonObjectPersisterFactory(application);
cacheManager.addPersister(jacksonObjectPersisterFactory);
return cacheManager;
}
#Override
public RestTemplate createRestTemplate() {
final RestTemplate restTemplate = new RestTemplate();
// web services support json responses
final MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();
// web Services support also Text Messages ( for some error cases
final StringHttpMessageConverter stringConverter = new StringHttpMessageConverter(Charset.forName("ISO-8859-1"));
final List<HttpMessageConverter<?>> listHttpMessageConverters = restTemplate.getMessageConverters();
listHttpMessageConverters.add(jsonConverter);
listHttpMessageConverters.add(stringConverter);
restTemplate.setMessageConverters(listHttpMessageConverters);
return restTemplate;
}
}
As you can see I have added a stringConverter too, but unfortunately it did not resolve the problem.
public class RegisterCarRequest extends BaseCarSpiceRequest<RegisterCarResult> {
RegisterCarInput input;
private final Context ctx;
public RegisterDeviceRequest(RegisterCarInput input, Context ctx) {
super(RegisterCarResult.class);
this.input = input;
this.ctx = ctx;
}
#Override
public RegisterCarResult loadDataFromNetwork() throws Exception {
final String url = ctx.getString(R.string.base_url) + "/cars";
final HttpEntity<?> requestEntity = new HttpEntity<Object>(input, getRequestHeaders());
final ResponseEntity<RegisterCarResult> response = getRestTemplate().exchange(url, HttpMethod.POST, requestEntity,
RegisterCarResult.class);
return response.getBody();
}
}
The pojo for sending to the server is this one:
public class RegisterCarInput {
private String carId;
public String getCarId() {
return carId;
}
public void setCarId(String carId) {
this.carId = carId;
}
}
the pojo for the Response is this Object:
public class RegisterCarResult {
private Boolean magicQuestionDefined;
private Boolean newCar;
public Boolean getMagicQuestionDefined() {
return magicQuestionDefined;
}
public void setMagicQuestionDefined(Boolean magicQuestionDefined) {
this.magicQuestionDefined = magicQuestionDefined;
}
public Boolean getNewDevice() {
return newCar;
}
public void setNewCar(Boolean newCar) {
this.newCar = newCar;
}
}
The Request gets called like this:
private void performRequest(RegisterCarInput input, User user) {
final RegisterCarRequest request = new RegisterCarRequest(input, getApplicationContext());
request.setAuthUser(user);
spiceManager.execute(request, new RegisterCarRequestListener());
}
Does anybody see a missing link for being able to parse different structures of responses?
Is it really necessary (for robospice) to receive httpstatuscodes (with a
content-type=application/Json for errors with no payload and a 401 status code) ?
http states in the Listener when the backend returns with a 401 ?
tia
Luke
EDITED:
I did some investigation and can now say that the backend returns a 403 when my basic authentication is wrong and sends following response:
+ Response 403 (text/html)
HTTP/1.1 403 Forbidden
Date: Wed, 12 Nov 2014 08:26:14 GMT
Server: Apache
Transfer-Encoding: chunked
Content-Type: text/html
97
<html>
<head>
<title>
403 Forbidden
</title>
</head>
<body>
<h1>Forbidden</h1>
You tried to access a page without having the required permissions.<br>
</body>
</html>
Unfortunately I don't have any influence to the behavior of the backend.
Facts:
The calls are working when everything is ok (200) and receiving RegisterCarResult as json
The calls are working when only a status code (401) (with no content in the response entity) is returned.
The calls are failing when a status code (403) with html content is returned in the entity
So the question, which answer will solve my problem is this:
How can I handle RestResponses, which can have different content-types (application/json)(text/html) in their entities with ROBOSPICE?
I want to send a Json with a request to the backend => ok
I want to receive a 200 as response and a application/json in the response entity => ok
I want to receive only a status code 401 with no content => ok
I want to receive a status code 403 with text/html content => HOW?
All those responses(2,3,4) are possible answers to the one request(1)...
My problem look like this: When i send POST request to REST Service I get Access-Control-Allow-Origin error, but request GET it's working.
This is my Rest Service:
#Path("/createUser")
#RequestScoped
public class ClientRestService {
#Inject
private ClientManager clientManager;
#POST
#Path("{name}/{surname}/{adress}")
#Produces(MediaType.APPLICATION_JSON)
public Response createUser(#PathParam("name")String name, #PathParam("surname")String surname, #PathParam("adress")String adress) {
Client client = new Client(name,surname,adress);
Response.ResponseBuilder builder = Response.ok("POST It's working.!!!");
builder.header("Access-Control-Allow-Origin", "*");
builder.header("Access-Control-Allow-Methods", "POST");
return builder.build();
}
#GET
public Response getMetod() {
Response.ResponseBuilder builder = Response.ok("GET It's working.");
builder.header("Access-Control-Allow-Origin", "*");
return builder.build();
}
}
This is client:
$(document).ready(function() {
$.ajax({
url: 'http://localhost:8080/Bank_Project_Test/MyApp/createUser',
type: 'POST',
data: 'name=SomeName'+
'&surname=SomeSurname'+
'&adress=SomeAdress',
success: function(success) {
alert(success);
}
});
});
Might fail because of cors preflight requests?
If you see OPTIONS-method requests in the browser when things fail, then you need to handle those in your server implementation as well.
https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Preflighted_requests