swagger-ui icon indicating copy to clipboard operation
swagger-ui copied to clipboard

redirect endpoint returning a 302 redirect to AWS S3 - generating an auth problem

Open kobymeir opened this issue 6 years ago • 16 comments

I have a download endpoint in my API which is redirecting the user to a AWS S3 presigned URL

Here is the swagger file describing my endpoint:

openapi: 3.0.0
info:
  title: My API
  description: API
  version: 2.0

servers:
  - url: myapi.com
    description: API v2.0.
components:
  securitySchemes:
    Auth:
      type: apiKey
      in: header
      name: Authorization
security:
  - Auth: []
paths:
  /download/:
    get:
      summary: Download
      description: Download
      responses:
        '302':
          description: Redirects to a location for downloading
          content:
            application/gzip:
              schema:
                type: string
                format: binary

My problem is when the SwaggerUI (version 3.14.2) it trying the endpoint it does get the redirect order, but when it tries to go the redirect location it for some reason sends the "Authorization" header to that URL although it's not on the same domain.

This problem is causing AWS S3 to omit the following error because it's receiving both "Authorization" header and the "AWSAccessKeyId" get parameters.

<?xml version="1.0"?>
<Error>
  <Code>InvalidArgument</Code>
  <Message>Only one auth mechanism allowed; only the X-Amz-Algorithm query parameter, Signature query string parameter or the Authorization header should be specified</Message>
  <ArgumentName>Authorization</ArgumentName>
  <ArgumentValue>Token TTTTTTTTTTTTT</ArgumentValue>
  <RequestId>RRRRRRRRRRRRRR</RequestId>
  <HostId>HHHHHHHHHHHHHHHH</HostId>
</Error>

Any idea how to solve this issue?

kobymeir avatar Jun 05 '18 18:06 kobymeir

Hey @kobymeir!

Transparently following the redirect your server provides is intended behavior. I've mentioned this before over in Swagger Client - see https://github.com/swagger-api/swagger-js/issues/1030#issuecomment-312728833.

I'm curious what led you to the expectation that the Authorization header wouldn't be forwarded to the redirect location - if you can shed some light on that, it'd be helpful in considering whether this is something we should explore changing 😄

shockey avatar Jul 25 '18 00:07 shockey

Hey shockey, we have the same problem here. Currently you cannot use Swagger-Ui with AWS, if you have such an authorization step.

Swagger-Ui sends the original authorization token together with the redirect authorization token to AWS. AWS complains with a 400 bad request. Swagger-Ui should send only the redirect authorization token in the final request, then it would work.

Currently, we are forced to use Postman instead of Swagger-Ui, because of this problem. It would be nice, if this gets fixed soon.

MaxHo1234 avatar Aug 06 '18 13:08 MaxHo1234

Hi @shockey

There is no reason to pass on the authorization headers as you have no idea where the redirect it going, thus potentially exposing the authorization token to a 3rd party that we might not want them to receive the token.

Hope that my answered helped :)

kobymeir avatar Aug 06 '18 14:08 kobymeir

Postman had the problem in the past: https://github.com/postmanlabs/postman-app-support/issues/1160

MaxHo1234 avatar Aug 07 '18 16:08 MaxHo1234

@kobymeir @MaxHo1234, upon further inspection, this is a limitation within the browser. The request is being transparently followed, with no way to control the behavior from within a web application like Swagger UI. Postman gets around this with their Interceptor extension, which is not subject to the same constraints.

I'm going to keep this open: if there's sufficient interest, we could consider building a similar extension that allows users to circumvent browser limitations.

shockey avatar Aug 28 '18 20:08 shockey

This is a few months late, but I had this same problem and found a fix - thought I'd update anyone who comes across this page. The problem seems to be when using an 'Authorization' header to access your api then getting redirected to a signed url. There is a conflict because your browser (Chrome in my case) doesn't like that there are two separate authorizations: the signed url authorizations and the header you passed called 'Authorization' - as @shockey mentioned.
The easy fix for me was to change my 'Authorization' header to something different...'usertoken' in my case. Then update your API to authorize based on your new header and boom! It works like a charm.

dalearbo avatar Jan 31 '19 19:01 dalearbo

I've hit this problem today, and I feel like @dalearbo 's solution would be inappropriate: I certainly won't change my server and API code to accommodate the lack of forwarding of the Authorization header.

However, while digging further I realized that the problem was my fault: my server was redirecting to the wrong address (https://example.com/api/v1/blah) whereas it should have been (http://... ). In order words, I was mixing my https and http server. After fixing this small problem, the redirect works as expected, keeping the Authorization header.

Just a heads up for people in the future how stumble into this issue.

dojeda avatar Feb 07 '19 14:02 dojeda

@dojeta: Thanks for your comment. Just to be clear, my solution does not drop the authorization header. What you’re doing is just calling your authorization header something else because “Authorization” is a reserved keyword. You can literally call it anything else and you’re still sending it - your server is still requiring the header that you choose so there is no security issue created. I think that we are basically troubleshooting separate issues but it should be known that sending an “Authorization” header in addition to a signed URL is not possible in Chrome.

dalearbo avatar Feb 07 '19 15:02 dalearbo

Side note: application/gzip on 302 doesn't make any sense.

michael-o avatar May 19 '20 15:05 michael-o

We've just run into this problem. In response to this:

I'm curious what led you to the expectation that the Authorization header wouldn't be forwarded to the redirect location - if you can shed some light on that, it'd be helpful in considering whether this is something we should explore changing

Funny that this discussion has happened in GitHub issues and StackOverflow threads for all kinds of HTTP clients, but usually not forwarding the header is considered the default, and the discussion proceeds from there ... :-)

I wish the HTTP spec was more specific about it.

But in any case, Swagger is making an unusual choice by forwarding our Authorization header to S3. Chrome, Safari and Internet Explorer don't work that way. Python Requests and Postman also fixed this problem -- Postman has made it an opt-in behaviour. Curl actually treated it as a security issue and removed the behaviour a couple of years ago.

teajaymars avatar Sep 30 '20 18:09 teajaymars

As I committer of Apache HttpComponents: HttpClient will never do this, unless you wide-scope your credentials. Similar issue: https://issues.apache.org/jira/browse/WAGON-590

michael-o avatar Sep 30 '20 19:09 michael-o

I too came across this problem and realized that when fetch redirects to the presigned S3 URL you can't prevent it from sending the Authorization headers from your API.

Eventually I have able to get this working by using the responseInterceptor configuration argument for Swagger with a custom function that detects the bad request (400) response from S3 and then re-issues the fetch request with credentials: 'omit'.

Here is my custom response interceptor for Swagger:

// swagger-ui-extensions.js

function serializeHeaderValue(value) {
  const isMulti = value.includes(', ');
  return isMulti ? value.split(', ') : value;
}

function serializeHeaders(headers = {}) {
  return Array.from(headers.entries()).reduce((acc, [header, value]) => {
    acc[header] = serializeHeaderValue(value);
    return acc;
  }, {});
}

function myResponseInterceptor(response) {
  // NOTE: Additional checks should probably be added whether to re-issue the fetch. This was just an initial starting point.
  if (response.ok === false && response.status === 400 && response.headers['server'] === 'AmazonS3') {
    // Here is the important part, re-issue fetch but don't allow our Authentication header to flow
    response = fetch(response.url, { credentials: 'omit' })
      .then(nativeResponse => {
        // We can't return the native response because Swagger UI attempts to assign the header property (and potentially other properties
        // too) on the response. So return a serialized clone of the native response. FYI, this is the same exact logic from Swagger's fake
        // implementation of fetch.
        const getBody = nativeResponse.blob || nativeResponse.buffer;
        return getBody.call(nativeResponse).then(body => {
          return {
            ok: nativeResponse.ok,
            url: nativeResponse.url,
            status: nativeResponse.status,
            statusText: nativeResponse.statusText,
            headers: serializeHeaders(nativeResponse.headers),
            data: body
          };
        });
      });
  }
  return response;
}

Then I had to specify my custom myResponseInterceptor when initializing the Swagger UI in index.html

      // (other code omitted for brevity...)

      // Make sure to include your custom JS in the page
      // <script src="./swagger-ui-extensions.js"></script>

      // Specifying the custom responseInterceptor here...
      configObject.responseInterceptor = myResponseInterceptor;

      // Begin Swagger UI call region

      const ui = SwaggerUIBundle(configObject);

      ui.initOAuth(oauthConfigObject);

      // End Swagger UI call region

      window.ui = ui;

I was using ASP.NET Core and used these instructions to provide my own index.html for Swagger UI: https://github.com/domaindrivendev/Swashbuckle.AspNetCore#customize-indexhtml

After all that, this surprisingly worked and I was able to see the redirected response from S3 in Swagger.

polewskm avatar Nov 27 '20 22:11 polewskm

I have the same problem with Azure Blob Storage Shared Access Signatures.

Skleni avatar Feb 12 '21 11:02 Skleni

Same problem with MinIO. It is necessary to somehow remove Authorization from header in redirected request.

nnsns avatar Feb 16 '21 20:02 nnsns

I was able to also solve the issue with MinIO by using the responseInterceptor solution mentioned up above.

polewskm avatar Feb 16 '21 21:02 polewskm

Have you needed to update the code you posted above since you posted it, @polewskm ? I ask as I'm trying to use your fix, and, while the re-fetch appears to work, Swagger cannot deal with the response and shows the message "Could not render n, see the console" rather than initiate the S3 file download.

TrevorT avatar Aug 24 '22 09:08 TrevorT