packages feed

persona-idp-0.1.0.1: src/Middleware.hs

-- This file is part of persona-idp - Persona (BrowserID) Identity Provider
-- Copyright (C) 2015  Fraser Tweedale
--
-- persona-idp is free software: you can redistribute it and/or modify
-- it under the terms of the GNU Affero General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-- GNU Affero General Public License for more details.
--
-- You should have received a copy of the GNU Affero General Public License
-- along with this program.  If not, see <http://www.gnu.org/licenses/>.

{-# LANGUAGE OverloadedStrings #-}

module Middleware
  (
    remoteUserX509Middleware
  , certLookupEmail
  ) where

import Control.Applicative
import Control.Monad
import Data.Foldable
import Data.Monoid

import Data.ASN1.Types.String
import qualified Data.ByteString as B
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import Data.PEM
import Data.X509
import Network.HTTP.Types
import Network.Wai

data RemoteUserLookup = RemoteUserLookup
  HeaderName
  (B.ByteString -> Maybe B.ByteString)

remoteUserLookupX509 :: RemoteUserLookup
remoteUserLookupX509 = RemoteUserLookup "SSL_CLIENT_CERT" f
  where
  f = either (const Nothing) getEmail
    . (decodeSignedCertificate <=< content <=< pemParseBS . B.map tabToLf)
  getEmail = fmap (T.encodeUtf8 . T.pack) . certLookupEmail . getCertificate
  content [] = Left "nothing to see here"
  content (x:_) = Right (pemContent x)
  tabToLf 9 = 10
  tabToLf c = c

certLookupEmail :: Certificate -> Maybe String
certLookupEmail a = sanEmail a <|> sdnEmail a
  where
  sdnEmail = asn1CharacterToString
    <=< lookup [1,2,840,113549,1,9,1] . getDistinguishedElements . certSubjectDN
  sanEmail = findEmailAltName <=< extensionGet . certExtensions
  findEmailAltName (ExtSubjectAltName names) =
    getFirst (foldMap (First . rfc822AltName) names)
  rfc822AltName (AltNameRFC822 s) = Just s
  rfc822AltName _ = Nothing

lookupRemoteUser :: RemoteUserLookup -> Request -> Maybe B.ByteString
lookupRemoteUser (RemoteUserLookup k f) =
  f <=< lookup k . requestHeaders

-- | Create a 'Middleware' from a 'RemoteUserLookup'
--
-- The middleware triggers if the request does not contain the
-- "REMOTE_USER" header.  The first successful lookup causes the
-- result to be stored in this header, so subsequent middlewares
-- will not have effect.
--
-- Note that header names are case-insensitive.
--
remoteUserLookupMiddleware :: RemoteUserLookup -> Middleware
remoteUserLookupMiddleware a = (. modreq)
  where
  modreq req =
    let
      hs = requestHeaders req
      k = "REMOTE_USER"
    in
      case lookup k hs of
        Just _ -> req
        Nothing -> case lookupRemoteUser a req of
          Nothing -> req
          Just s -> req { requestHeaders = (k, s) : hs }

-- | Copies email address from a PEM-encoded X.509 certificate
-- provided via the SSL_CLIENT_CERT header.  The Subject Alternative
-- Name extension is preferred over the (deprecated) Subject DN
-- email component.
--

remoteUserX509Middleware :: Middleware
remoteUserX509Middleware =
  remoteUserLookupMiddleware remoteUserLookupX509