ThreeMammals / Ocelot

.NET API Gateway
https://www.nuget.org/packages/Ocelot
MIT License
8.38k stars 1.64k forks source link

Why is the DownstreamHeaderTransform ignored? #1704

Closed skulidropek closed 1 year ago

skulidropek commented 1 year ago

Hello everyone. I have been struggling with this problem for a long time. I implemented authorization for Ocelot using Bearer token and everything works. But it stops working when I try to use DownstreamHeaderTransform or UpstreamHeaderTransform

"DownstreamHeaderTransform": {
 "Authorization": "Bearer myDownStreamToken"
},

If I use Upstream, then it simply replaces my token for authorization, which I transmit, which is logical in principle, because I'm trying to influence the upper header. But when I use the Downstream nothing works, even if I remove the authorization from the Downstream nothing works, which is strange. I just want to pass a custom authorization token for Downstream

{
  "Version": "1.1",
  "Content": {
    "Headers": [
      {
        "Key": "Content-Length",
        "Value": [ "198" ]
      },
      {
        "Key": "Content-Type",
        "Value": [ "application/json; charset=utf-8" ]
      }
    ]
  },
  "StatusCode": 401,
  "ReasonPhrase": "Unauthorized",
  "Headers": [
    {
      "Key": "Connection",
      "Value": [ "keep-alive" ]
    },
    {
      "Key": "Date",
      "Value": [ "Mon, 25 Sep 2023 07:42:45 GMT" ]
    },
    {
      "Key": "Server",
      "Value": [ "cloudflare" ]
    },
    {
      "Key": "Alt-Svc",
      "Value": [ "h3=\":443\"" ]
    },
    {
      "Key": "Vary",
      "Value": [ "Origin" ]
    },
    {
      "Key": "X-Request-ID",
      "Value": [ "f65e86fd57d210848790f72b834d730a" ]
    },
    {
      "Key": "Strict-Transport-Security",
      "Value": [ "max-age=15724800; includeSubDomains" ]
    },
    {
      "Key": "CF-Cache-Status",
      "Value": [ "DYNAMIC" ]
    },
    {
      "Key": "CF-RAY",
      "Value": [ "80c1a1ddde3c345d-NRT" ]
    },
    {
      "Key": "Authorization",
      "Value": [ "Bearer myDownStreamToken" ]
    }
  ],
  "TrailingHeaders": [],
  "RequestMessage": {
    "Version": "1.1",
    "VersionPolicy": 0,
    "Content": {
      "ObjectType": "OpenAIFeedbackAnalyzer.Model.Request, OpenAIFeedbackAnalyzer, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
      "Value": {
        "ModelId": "gpt-3.5-turbo",
        "Messages": [
          {
            "Role": "user",
            "Content": "Hello"
          }
        ]
      },
      "Headers": [
        {
          "Key": "Content-Type",
          "Value": [ "application/json; charset=utf-8" ]
        }
      ]
    },
    "Method": { "Method": "POST" },
    "RequestUri": "http://DOMAIN/openai/v1/chat/completions",
    "Headers": [
      {
        "Key": "Authorization",
        "Value": [ "Bearer myUpStreamToken" ]
      },
      {
        "Key": "Transfer-Encoding",
        "Value": [ "chunked" ]
      }
    ],
    "Properties": {},
    "Options": {}
  },
  "IsSuccessStatusCode": false
}

I constantly get a similar response from the server. Although my Downstream is added to the headers, it is never read by the OpenAI server

{
  "Headers": [
    // Other headers
    {
      "Key": "Authorization",
      "Value": [ "Bearer myDownStreamToken" ]
    }
  ],
}

My upstream token is added to RequestMessage

"RequestMessage": {
  "Headers": [
  // Other headers
    {
      "Key": "Authorization",
      "Value": [ "Bearer myUpStreamToken" ]
    }
  ]
}

I have already read this documentation several times but have not found a solution to my problem https://ocelot.readthedocs.io/en/latest/features/headerstransformation.html

The message that the server returns to me Request finished HTTP/1.1 POST http://DOMAIN/openai/v1/chat/completions application/json;+charset=utf-8 - - 401 198 application/json;+charset=utf-8 171.8120ms

My config:

{
  "Routes": [
    /* OPEN AI */
    {
      "DownstreamPathTemplate": "/{everything}",
      "DownstreamScheme": "https",
      "DownstreamHostAndPorts": [
        {
          "Host": "api.openai.com",
          "Port": 443
        }
      ],
      "DownstreamHeaderTransform": {
        "Authorization": "Bearer myDownStreamToken"
      },
      "UpstreamPathTemplate": "/openai/{everything}",
      "UpstreamHttpMethod": [ "GET", "POST", "PUT", "DELETE" ],
      "AuthenticationOptions": {
        "AuthenticationProviderKey": "Bearer",
        "AllowedScopes": []
      },
      //"RouteClaimsRequirement": {
      //  "Role": "Administrator"
      //},
      "SwaggerKey": "openai"
    }
  ],
  "SwaggerEndPoints": [
    {
      "Key": "openai",
      "Config": [
        {
          "Name": "OpenAI API",
          "Version": "v1",
          "Url": "https://www.dropbox.com/scl/fi/qr7vnh7wl21o8ymiohha3/openapi-2.json?rlkey=e6kfeph7bybx1nyp1gyj8ulrk&dl=1"
        }
      ]
    }
  ],
  "GlobalConfiguration": {
    "BaseUrl": "URL"
  }
}
skulidropek commented 1 year ago

I was able to fix it for myself in this way: I just added the JWT authorization code so that it would accept my custom header

services.AddAuthentication(o =>
{
    o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(o =>
{
    o.RequireHttpsMetadata = false;
    o.SaveToken = true;
    o.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters()
    {
        ValidateIssuerSigningKey = true,
        ValidateIssuer = false,
        ValidateAudience = false,
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(JwtTokenHandler.JWT_SECURITY_KEY))
    };
    o.Events = new JwtBearerEvents
    {
        OnMessageReceived = context =>
        {
            string authorization = context.Request.Headers["X-MyApp-Authorization"];

            if (string.IsNullOrEmpty(authorization))
            {
                context.NoResult();
                return Task.CompletedTask;
            }

            if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
            {
                context.Token = authorization.Substring("Bearer ".Length).Trim();
            }

            if (string.IsNullOrEmpty(context.Token))
            {
                context.NoResult();
                return Task.CompletedTask;
            }

            return Task.CompletedTask;
        }
    };
});

My config:

{
  "DownstreamPathTemplate": "/{everything}",
  "DownstreamScheme": "https",
  "DownstreamHostAndPorts": [
    {
      "Host": "api.openai.com",
      "Port": 443
    }
  ],
  "UpstreamHeaderTransform": {
    "Authorization": "Bearer OpenAIToken"
  },
  "UpstreamPathTemplate": "/openai/{everything}",
  "UpstreamHttpMethod": [ "GET", "POST", "PUT", "DELETE" ],
  "AuthenticationOptions": {
    "AuthenticationProviderKey": "Bearer",
    "AllowedScopes": []
  },
  //"RouteClaimsRequirement": {
  //  "Role": "Administrator"
  //},
  "SwaggerKey": "openai"
}
raman-m commented 1 year ago

Dear Skuli,

You should understand that before making any requests through gateway app, you must check direct connection to downstream service from client apps. If the direct connection works then you are able to try to route this service traffic via gateway, applying correct configuration, for sure.

raman-m commented 1 year ago

@skulidropek commented on Sep 25 I just added the JWT authorization code so that it would accept my custom header

Definitely! As I said, you have to check direct connection first. And your header is X-MyApp-Authorization and your value is Bearer bla-bla-bla. But I cannot get it, why do you parse header value? Client app could forward token value only bla-bla-bla and you need not such complex design of authorization setup as AddJwtBearer callback, with custom parsing of the header. Truly speaking, it is a bit long and complicated.

I guess, this question-issue can be closed, right? Going to close...

raman-m commented 1 year ago

@skulidropek Next time, please open questions in Discussions space aka Q&A category!