3

Description

I have an API which I have created and I have some endpoints protected. The problem I am facing now on the client making a request is that the first request comes through with the Authorization header provided but a second request is blocked because Authorization is not present. I can confirm that Authorization is present and worked perfectly when I was running Typescript till I recreated the endpoints in Go with Gin.

How to reproduce

  • Call estimate endpoint from client (iOS app) response succeceds
  • Make a second call from Client (iOS app) response failed because it is not taking the Authorization header which contains token
package main

import (
    "github.com/gin-gonic/gin"
)

type App struct {
    Router   *gin.Engine
    Gateman  *gateman.Gateman
    Database *mongo.Client
}

func (a *App) StartApp() {

    err := godotenv.Load()
    if err != nil {
        fmt.Printf("Could not load .env \n")
    }

    a.Database = database.DB
    a.Router = gin.New()

    a.Gateman = middleware.Gateman()
    a.Router.Use(gin.Recovery())

    a.Router.Use(middleware.DefaultHelmet())

    a.Router.Use(middleware.GinContextToContextMiddleware())
    a.Router.Use(middleware.RequestID(nil))
    a.Router.Use(middleware.ErrorHandler())
    a.Router.Use(middleware.Logger("package-service"))

    connection, err := amqp091.Dial(os.Getenv("AMQP_URL"))

    if err != nil {
        log.Fatal(fmt.Printf("Error on dial %v\n", err.Error()))
    }
    routes.Routes(a.Router, database.GetDatabase(a.Database), a.Gateman, connection)

}

func (a *App) Run(addr string) {
    logs := log.New(os.Stdout, "package-service", log.LstdFlags)

    server := &http.Server{
        Addr:         addr,
        Handler:      a.Router,
        ErrorLog:     logs,
        IdleTimeout:  120 * time.Second, // max time for connections using TCP Keep-Alive
        ReadTimeout:  5 * time.Second,
        WriteTimeout: 10 * time.Second,
    }

    go func() {
        if err := server.ListenAndServe(); err != nil {
            logs.Fatal(err)
        }
    }()

    // trap sigterm or interrupt and gracefully shutdown the server
    c := make(chan os.Signal)
    signal.Notify(c, os.Interrupt)
    signal.Notify(c, os.Kill)

    sig := <-c
    logs.Println("Recieved terminate, graceful shutdown", sig)
    tc, _ := context.WithTimeout(context.Background(), 30*time.Second)
    server.Shutdown(tc)
}

func Routes(r *gin.Engine, db *mongo.Database, g *gateman.Gateman, conn *amqp091.Connection) {

    atHandler := pc.NewPackagesController(ps.NewPackagesService(pr.NewPackagesRepository(db)), g, events.NewEventEmitter(conn))

    r.Use(CORS())
    v1 := r.Group("/api/v1/package")
    {
        v1.POST("/query", GraphqlHandler(db, directives.NewDirectivesManager(g)))
        v1.GET("/", PlaygroundHandler(db))
        v1.POST("/", g.Guard([]string{"user"}, nil), atHandler.Create)
        v1.POST("/estimate", g.Guard([]string{"user"}, nil), atHandler.Estimate)
        v1.PUT("/:packageID", g.Guard([]string{"user", "admin"}, nil), atHandler.Update)
        v1.PUT("/:packageID/assign", g.Guard([]string{"admin"}, nil), atHandler.Assign)
        v1.POST("/:packageID/cancel", g.Guard([]string{"user", "admin"}, nil), atHandler.CancelRequest)
        v1.POST("/:packageID/complete", g.Guard([]string{"admin"}, nil), atHandler.Complete)
        v1.POST("/:packageID/reject", g.Guard([]string{"admin"}, nil), atHandler.RejectRequest)

        v1.GET("/healthz", atHandler.GetHealth)

    }
    r.GET("/", atHandler.GetUP)
}

func CORS() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
        c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
        c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
        c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")

        if c.Request.Method == "OPTIONS" {
            c.AbortWithStatus(204)
            return
        }

        c.Next()
    }
}

func main() {
    start := App{}
    start.StartApp()
    start.Run(":3009")
}

Expectations

All endpoints that are Guarded simply checks the header for Authorization and if provided in the request, it should be successful

Actual result

First request succeed /estimate Second request / POST request fails to accept Authorization header

Also irrespective of what the first post request is, the second post request just never accept the Authorization header

Also need to mention that I do not have this issue with postman. Both request run independently but using another client for the request, gives this problem

Environment

  • go version: 1.19
  • gin version (or commit ref): v1.8.1
  • operating system: Mac and iOS mobile

Here is my client code

func request<T>(with builder: BaseRequest) -> AnyPublisher<T, APIError> where T: Codable {
        request(with: builder, customDecoder: JSONDecoder())
    }
    func request<T>(with builder: BaseRequest, customDecoder: JSONDecoder) -> AnyPublisher<T, APIError> where T: Codable {
        
        let encoding: ParametersEncoder = [.get, .delete].contains(builder.method) ? URLParameretersEncoder() : JSONParametersEncoder()
        customDecoder.keyDecodingStrategy = .convertFromSnakeCase
        var url: URL {
            var components = URLComponents()
            components.scheme = "https"
            components.host = builder.baseUrl
            components.path = "/api/v1" + builder.path
            
            guard let url = components.url else {
                preconditionFailure("Invalid URL components: \(components)")
            }
            
            return url
        }
        var urlRequest = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, timeoutInterval: 46.0)
        urlRequest.httpMethod = builder.method.rawValue
        builder.headers.forEach { key, value in
            urlRequest.setValue(value, forHTTPHeaderField: key)
        }
        if let token = tokenManager.token {
            urlRequest.setValue("Bearer " + token, forHTTPHeaderField: "Authorization")
//            urlRequest.setValue("ABC", forHTTPHeaderField: "testing123")
        }
        
        if let parameters = builder.parameters {
            guard let encoded = try? encoding.encode(parameters: parameters, in: urlRequest) else {
                fatalError()
            }
            urlRequest = encoded
        }
        self.log(request: urlRequest)
        return URLSession.shared
            .dataTaskPublisher(for: urlRequest)
            .receive(on: DispatchQueue.main)
            .mapError {_  -> APIError in
                UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
                return .unknown
                
            }
            .flatMap { data, response -> AnyPublisher<T, APIError> in
                guard let response = response as? HTTPURLResponse else {
                    UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
                    return Fail(error: APIError.invalidResponse).eraseToAnyPublisher()
                }
                self.log(response: response, data: data, error: nil)
                if (200...299).contains(response.statusCode) {
                    return Just(data)
                        .decode(type: T.self, decoder: customDecoder)
//                        .map {
//                            print($0)
//                            return $0
//                        } //added
                        .mapError {
                            UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
                            return .decodingError(underlyingError: $0)
                            
                        }
                    
                        .eraseToAnyPublisher()
                } else {
                    UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
                    if response.statusCode == 401 {
                        // Send notification to remove corrdinator and return to root
//                        rxNotificationCenter.post(.invalidToken)
                    }
                    guard let errorResponse = try? customDecoder.decode(BaseResponse.self, from: data) else {
                        return Fail(error: APIError.decodingError(underlyingError: NSError("Can't decode error"))).eraseToAnyPublisher()
                    }
                    return Fail(error: APIError.server(response: errorResponse))
                        .eraseToAnyPublisher()
                }
            }
            .eraseToAnyPublisher()
    }


protocol BaseRequest {
    var baseUrl: String { get }
    var path: String { get }
    var headers: HTTPHeaders { get }
    var method: HTTPRequestMethod { get }
    var parameters: HTTPParameters { get }
}

public typealias HTTPHeaders = [String: String]
public typealias HTTPParameters = [String: Any]?

Another point, calling a single endpoint multiple time, works fine, calling a different one is where the header is rejected

King
  • 1,885
  • 3
  • 27
  • 84
  • I doubt the issue is on the server-side, so I would investigate & update the question to show your iOS client-side code. – colm.anseo Dec 14 '22 at 22:39
  • I would update it. The authorization works for the first request but does not not for the second one, I then passed a dummy header as part of the request header and the dummy header is present but Authorization is not. Also when I changed the name from just `Authorization` to `Authorizationxzy`, I see the value. – King Dec 14 '22 at 22:46
  • Another point, calling a single endpoint multiple time, works fine, calling a different one is where the header is rejected – King Dec 14 '22 at 23:05
  • 1
    I think I can pin the problem down now. A weird problem. In my request, I am using `/api/v1/package` this was causing a 307 redirect. It expected me to use `/api/v1/package/` which is weird. – King Dec 15 '22 at 00:04
  • See [RedirectTrailingSlash](https://pkg.go.dev/github.com/gin-gonic/gin#Engine.RedirectTrailingSlash) in the engine configuration. – Charlie Tumahai Dec 15 '22 at 02:29

0 Answers0