Niko
Niko

Reputation: 810

Akka http 401 on not authenticated endpoint

I have the following routes definition

private val routes: Route =
    concat(
      pathEnd {
        get {
          handleErrorsAndReport("list_foos") {
            
          }
        }
      },
      path(Segment) { fooId =>
        get {
          handleErrorsAndReport("get_foo") {
            rejectEmptyResponse(
              complete(service.do(fooId))
            )
          }
        }
      },
      pathEnd {
        authenticateBasic(
          realm = "Secure scope",
          scopesAuthenticator
        ) { scopes =>
          post {
            handleErrorsAndReport("create_foo") {
             
            }
          }
        }
      },
      path(Segment) { fooId =>
        authenticateBasic(
          realm = "Secure scopes",
          scopesAuthenticator
        ) { scopes =>
          concat(
            put {
              handleErrorsAndReport("update_foo") {
              
              }
            },
            delete {
              handleErrorsAndReport("delete_foo") {
               
              }
            }
          )
        }
      }
    )

I am trying to consume the get_foo endpoint. I have created a unit test for that and it looks like this

"allow get operation on foo without authentication present" in {
      Get("/foos/{some_id}") ~> routes ~> check {
        status shouldBe StatusCodes.NotFound
      }
    }

While debugging the test I can see that the route is correctly identified and I can access the code inside the route. The service code inside the get_foo route produces a None and complete(None) creates a rejection since it's an empty response and I have the rejectEmptyResponse directive. So I would expect that I would get a 404 response based on the handleErrorsAndReport directive that I have defined. The error handler looks like this

  private def handleErrorsAndReport(endpoint: String): Directive0 = extractRequestContext.flatMap { ctx =>
    val start = System.currentTimeMillis()
    mapResponse { resp =>
      // store response related metrics and return response
      resp
    } & handleExceptions(exceptionHandler)
  }

  private val exceptionHandler: ExceptionHandler = {

    def handle(e: Throwable, responseCode: StatusCode, errorMessage: Option[String]): Route = {
      extractRequest { request =>        

        val response = (responseCode, errorMessage) match {
          case (InternalServerError, _) => "Internal Server Error"
          case (_, Some(message)) => message
          case _ => "Bad Request"
        }

        complete(HttpResponse(responseCode, entity = response))
      }
    }

    ExceptionHandler {
      case e@AError(description) => handle(e, BadRequest, Some(description))
      case e: BError => handle(e, InternalServerError, Some(e.errorMessage))
      case e: CError => handle(e, BadRequest, Some(e.errorMessage))
      case e: DError => handle(e, BadRequest, Some(e.errorMessage))
      case e: EError => handle(e, BadRequest, Some(e.errorMessage))
      case e@FException(filter) => handle(e, BadRequest, Some(s"bla"))
      case other => handle(other, InternalServerError, Option(other.getMessage))
    }
  }

What I am getting though is a 401 Unauthorized. How can this be? As I was debugging the code I noticed that the control flow never enters my exception handler - I added breakpoints everywhere inside...

Upvotes: 1

Views: 397

Answers (1)

zmerr
zmerr

Reputation: 574

The problem with your code is that, after the rejection of request in the unauthenticated directive, the match happens against an authenticated route with the same url. So the authentication fails and results in 401 unauthorized instead of 404.

For solving it, you need to prevent this match against the authenticated route, after the failure of the unauthenticated route with the GET method, by wrapping it inside post, put, and delete routes so that the GET requests cannot reach it.

So it can be written as

private val routes: Route =
    concat(
      pathEnd {
        get {
          handleErrorsAndReport("list_foos") {
            
          }
        }
      },
      path(Segment) { fooId =>
        get {
          handleErrorsAndReport("get_foo") {
            rejectEmptyResponse(
              complete(service.do(fooId))
            )
          }
        }
      },
      post {
         pathEnd {
          authenticateBasic(
            realm = "Secure scope",
            scopesAuthenticator
          ) { scopes =>
          
               handleErrorsAndReport("create_foo") {
               }
             
            }
         }
      },
      put{
         path(Segment) { fooId =>
           authenticateBasic(
           realm = "Secure scopes",
           scopesAuthenticator
          ) { scopes =>
          
              handleErrorsAndReport("update_foo") {

              }
            }
        }
      },
      delete{
         path(Segment) { fooId =>
           authenticateBasic(
           realm = "Secure scopes",
           scopesAuthenticator
          ) { scopes =>
          
              handleErrorsAndReport(“delete_foo") {

              }
           }
        }
      }
    )

Upvotes: 1

Related Questions