Skip to content

Commit

Permalink
Merge pull request #1670 from jmcardon/auth-spider
Browse files Browse the repository at this point in the history
Reintroduce the option for fallthrough for authenticated services
  • Loading branch information
aeons committed Feb 26, 2018
2 parents f85db4b + 7352bb2 commit 5fc2f82
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 4 deletions.
7 changes: 7 additions & 0 deletions docs/src/main/tut/auth.md
Expand Up @@ -55,6 +55,13 @@ val middleware: AuthMiddleware[IO, User] =
AuthMiddleware(authUser)
```

Note: In the above, the default apply method of `AuthMiddleware` will consume all requests either unmatched, or
not authenticated by returning an empty response with status code 401 (Unauthorized). This mitigates
a kind of reconnaissance called "spidering", useful for white and black hat hackers to enumerate
your api for possible unprotected points. To allow fallthrough,
use `AuthMiddleware.withFallThrough`. Alternatively, to customize the behavior on not authenticated if you do not
wish to always return 401, use `AuthMiddleware.noSpider` and specify the `onAuthFailure` handler.

Finally, we can create our `AuthedService`, and wrap it with our authentication middleware, getting the
final `HttpService` to be exposed. Notice that we now have access to the user object in the service implementation:

Expand Down
26 changes: 22 additions & 4 deletions server/src/main/scala/org/http4s/server/package.scala
Expand Up @@ -49,13 +49,31 @@ package object server {
type SSLBits = SSLConfig

object AuthMiddleware {

def apply[F[_]: Monad, T](
authUser: Kleisli[OptionT[F, ?], Request[F], T]
): AuthMiddleware[F, T] =
noSpider[F, T](authUser, defaultAuthFailure[F])

def withFallThrough[F[_]: Monad, T](
authUser: Kleisli[OptionT[F, ?], Request[F], T]): AuthMiddleware[F, T] =
service => {
Kleisli((r: Request[F]) => authUser(r).map(AuthedRequest(_, r)))
.andThen(service.mapF(o => OptionT.liftF(o.fold(Response[F](Status.NotFound))(identity))))
.mapF(o => OptionT.liftF(o.fold(Response[F](Status.Unauthorized))(identity)))
_.compose(Kleisli((r: Request[F]) => authUser(r).map(AuthedRequest(_, r))))

def noSpider[F[_]: Monad, T](
authUser: Kleisli[OptionT[F, ?], Request[F], T],
onAuthFailure: Request[F] => F[Response[F]]
): AuthMiddleware[F, T] = { service =>
Kleisli { r: Request[F] =>
authUser
.map(AuthedRequest(_, r))
.andThen(service.mapF(o => OptionT.liftF(o.getOrElse(Response[F](Status.NotFound)))))
.mapF(o => OptionT.liftF(o.getOrElseF(onAuthFailure(r))))
.run(r)
}
}

def defaultAuthFailure[F[_]](implicit F: Applicative[F]): Request[F] => F[Response[F]] =
_ => F.pure(Response[F](Status.Unauthorized))

def apply[F[_], Err, T](
authUser: Kleisli[F, Request[F], Either[Err, T]],
Expand Down
Expand Up @@ -193,6 +193,36 @@ class AuthMiddlewareSpec extends Http4sSpec {
Unauthorized)
}

"not consume the entire request when using fall through" in {

val authUser: Kleisli[OptionT[IO, ?], Request[IO], User] =
Kleisli.liftF(OptionT.none)

val authedService: AuthedService[User, IO] =
AuthedService {
case POST -> Root as _ => Ok()
}

val regularService: HttpService[IO] = HttpService[IO] {
case GET -> Root => Ok()
}

val middleware = AuthMiddleware.withFallThrough(authUser)

val service = middleware(authedService)

//Unauthenticated
(service <+> regularService).orNotFound(Request[IO](method = Method.POST)) must returnStatus(
NotFound)
//Matched normally
(service <+> regularService).orNotFound(Request[IO](method = Method.GET)) must returnStatus(
Ok)
//Unmatched
(service <+> regularService).orNotFound(Request[IO](method = Method.PUT)) must returnStatus(
NotFound)

}

}

}

0 comments on commit 5fc2f82

Please sign in to comment.