packages feed

sproxy 0.9.7.1 → 0.9.8

raw patch · 10 files changed

+55/−50 lines, 10 filesdep −raw-strings-qqdep −string-conversions

Dependencies removed: raw-strings-qq, string-conversions

Files

ChangeLog.md view
@@ -1,7 +1,20 @@+0.9.8+=====++* If the user is not authenticatedi, show login page with [HTTP status code+  511](https://tools.ietf.org/html/rfc6585), instead of 302 -> 200.  It had+  bad UX for AJAX calls.+* Always convert authenticated user's email to lowercase. This affects the+  cookie and the `From` header.+* Stop using the `string-conversions` package.+* Print authentication code in debug mode only.++ 0.9.7.1 =======  * Fixed cabal source distribution+  0.9.7 =====
README.md view
@@ -12,14 +12,14 @@  ## How it Works -When an HTTP client makes a request, Sproxy checks for a *session-cookie*.  If it doesn't exist (or it's invalid, expired), it redirects-the client to the login page, where the user can choose-[OAuth2](https://tools.ietf.org/html/rfc6749) provider to authenticate with.-Finally, we store the the email address in a session cookie: signed with a-hash to prevent tampering, set for HTTP only (to prevent malicious JavaScript-from reading it), and set it for secure (since we don't want it traveling-over plaintext HTTP connections).+When an HTTP client makes a request, Sproxy checks for a *session cookie*.+If it doesn't exist (or it's invalid, expired), it responses with [HTTP+status 511](https://tools.ietf.org/html/rfc6585) with the page, where the+user can choose an [OAuth2](https://tools.ietf.org/html/rfc6749) provider to+authenticate with.  Finally, we store the the email address in a session+cookie: signed with a hash to prevent tampering, set for HTTP only (to prevent+malicious JavaScript from reading it), and set it for secure (since we don't+want it traveling over plaintext HTTP connections).  From that point on, when sproxy detects a valid session cookie it extracts the email, checks it against the access rules, and relays the request to the
sproxy.cabal view
@@ -1,5 +1,5 @@ name: sproxy-version: 0.9.7.1+version: 0.9.8 synopsis: HTTP proxy for authenticating users via OAuth2 license: MIT license-file: LICENSE@@ -54,10 +54,8 @@     , interpolatedstring-perl6     , network     , postgresql-simple-    , raw-strings-qq >= 1.1     , resource-pool     , split-    , string-conversions     , text     , time     , tls >= 1.3.3
src/Authenticate.hs view
@@ -4,15 +4,13 @@  module Authenticate (   logout-, loginPage-, redirectToLoginPage+, authenticationRequired , validAuth ) where  import Data.ByteString (ByteString)-import Data.Monoid ((<>)) import Network.HTTP.Toolkit (Response, BodyReader)-import Network.HTTP.Types (found302, ok200, urlEncode)+import Network.HTTP.Types (found302, Status(..)) import System.Posix.Time (epochTime) import Text.InterpolatedString.Perl6 (qc) import Text.Read (readMaybe)@@ -25,15 +23,9 @@ import qualified Authenticate.Google as Google import qualified Authenticate.LinkedIn as LinkedIn -redirectToLoginPage :: ByteString -> ByteString -> IO (Response BodyReader)-redirectToLoginPage base path =-  mkResponse found302 [-    ("Location", base <> "/sproxy/login?state=" <> urlEncode True path)-  ] ""--loginPage :: AuthConfig -> ByteString -> ByteString -> IO (Response BodyReader)-loginPage c base path =-  mkHtmlResponse ok200 body+authenticationRequired :: AuthConfig -> ByteString -> ByteString -> IO (Response BodyReader)+authenticationRequired c base path =+  mkHtmlResponse (Status 511 "Authentication Required") body   where     google :: ByteString     google = maybe "" (\u -> [qc|<p><a href="{u}">Authenticate with Google</a></p>|]) (Google.authUrl c base path)
src/Authenticate/Google.hs view
@@ -10,6 +10,7 @@ import Control.Exception (try) import Data.Aeson (FromJSON, decode, parseJSON, Value(Object), (.:)) import Data.ByteString (ByteString)+import Data.Char (toLower) import Data.Maybe (fromJust) import Data.Monoid ((<>)) import Network.HTTP.Toolkit (Response, BodyReader)@@ -37,7 +38,7 @@  authenticate :: AuthConfig -> ByteString -> ByteString -> ByteString -> IO (Response BodyReader) authenticate acfg base path code = do-  Log.info ("Google authentication request with code " ++ show code)+  Log.debug ("Google authentication request with code " ++ show code)   tokenRes <- try $ post "https://accounts.google.com/o/oauth2/token"               ("code=" <> code <> "&client_id=" <> clientId                 <> "&client_secret=" <> clientSecret <> "&redirect_uri="@@ -57,7 +58,7 @@               case decode (HTTP.responseBody uresp) of                 Nothing -> authenticationFailed "Received an invalid user info response from Google's authentication server."                 Just (GoogleUserInfo{email, givenName, familyName}) -> do-                  token <- mkAuthToken acfg AuthUser{ authUserEmail = email+                  token <- mkAuthToken acfg AuthUser{ authUserEmail = map toLower email                                                     , authUserGivenName = givenName                                                     , authUserFamilyName = familyName }                   let cookie = setCookie cookieDomain cookieName (show token) (authConfigShelfLife acfg)
src/Authenticate/LinkedIn.hs view
@@ -10,6 +10,7 @@ import Control.Exception (try) import Data.Aeson (FromJSON, decode, parseJSON, Value(Object), (.:)) import Data.ByteString (ByteString)+import Data.Char (toLower) import Data.Maybe (fromJust) import Data.Monoid ((<>)) import Network.HTTP.Toolkit (Response, BodyReader)@@ -37,7 +38,7 @@  authenticate :: AuthConfig -> ByteString -> ByteString -> ByteString -> IO (Response BodyReader) authenticate acfg base path code = do-  Log.info ("LinkedIn authentication request with code " ++ show code)+  Log.debug ("LinkedIn authentication request with code " ++ show code)   tokenRes <- try $ post "https://www.linkedin.com/oauth/v2/accessToken"               ("code=" <> code <> "&client_id=" <> clientId                 <> "&client_secret=" <> clientSecret <> "&redirect_uri="@@ -59,7 +60,7 @@               case decode (HTTP.responseBody uresp) of                 Nothing -> authenticationFailed "Received an invalid user info response from LinkedIn authentication server."                 Just (LinkedInUserInfo{emailAddress, firstName, lastName}) -> do-                  token <- mkAuthToken acfg AuthUser{ authUserEmail = emailAddress+                  token <- mkAuthToken acfg AuthUser{ authUserEmail = map toLower emailAddress                                                     , authUserGivenName = firstName                                                     , authUserFamilyName = lastName }                   let cookie = setCookie cookieDomain cookieName (show token) (authConfigShelfLife acfg)
src/Authenticate/Types.hs view
@@ -7,7 +7,7 @@ ) where  import Control.Applicative (empty)-import Data.Aeson (FromJSON, parseJSON, Value(Object), (.:), (.:?))+import Data.Aeson (FromJSON, parseJSON, Value(Object), (.:)) import Data.ByteString (ByteString) import System.Posix.Types (EpochTime) 
src/Authorize.hs view
@@ -9,14 +9,14 @@ , withDatabaseAuthorizeAction ) where -import           Control.Exception-import           Data.ByteString (ByteString)-import           Network.HTTP.Types-import           Text.InterpolatedString.Perl6 (q)-import           Database.PostgreSQL.Simple-import           Data.Pool-import           Data.Time.Clock-import           Data.String.Conversions (cs)+import Control.Exception+import Data.ByteString (ByteString)+import Data.Pool+import Data.Time.Clock+import Database.PostgreSQL.Simple+import Network.HTTP.Types+import Text.InterpolatedString.Perl6 (q)+import qualified Data.ByteString.Char8 as B8  type AuthorizeAction = Email -> Domain -> RequestPath -> Method -> IO [Group] @@ -31,7 +31,7 @@ createConnectionPool database = createPool open close 1 connectionIdleTime connectionPoolSize   where     open :: IO Connection-    open = connectPostgreSQL (cs database)+    open = connectPostgreSQL (B8.pack database)      connectionPoolSize :: Int     connectionPoolSize = 5
src/Main.hs view
@@ -8,7 +8,7 @@ import Data.Version (showVersion) import Paths_sproxy (version) -- from cabal import System.Environment (getArgs)-import Text.RawString.QQ (r)+import Text.InterpolatedString.Perl6 (qc) import qualified System.Console.Docopt.NoTH as O  import Authorize (withDatabaseAuthorizeAction)@@ -17,7 +17,7 @@  usage :: String usage =  "SProxy " ++ showVersion version ++-  " HTTP proxy for authenticating users via OAuth2" ++ [r|+  " HTTP proxy for authenticating users via OAuth2" ++ [qc|  Usage:   sproxy [options]
src/Proxy.hs view
@@ -14,7 +14,6 @@ import Data.Map as Map (fromListWith, toList, insert, delete) import Data.Maybe import Data.Monoid-import Data.String.Conversions (cs) import GHC.IO.Exception import Network (PortID(..), connectTo) import Network.HTTP.Toolkit@@ -39,7 +38,7 @@ import qualified Network.TLS as TLS import qualified Network.TLS.Extra as TLS -import Authenticate (loginPage, logout, redirectToLoginPage, validAuth)+import Authenticate (logout, authenticationRequired, validAuth) import Authenticate.Token (AuthToken(..), AuthUser(..)) import Authenticate.Types (AuthConfig(..), OAuthClient(..)) import Authorize@@ -203,16 +202,15 @@                   case join $ lookup "code" query of                     Nothing -> Just <$> badRequest                     Just code -> Just <$> LinkedIn.authenticate authConfig base redirectPath code-                ["sproxy", "login"] -> Just <$> loginPage authConfig base redirectPath                 ["sproxy", "logout"] -> Just <$> logout authConfig (base <> redirectPath)                 ["robots.txt"] -> Just <$> mkTextResponse ok200 "User-agent: *\nDisallow: /"                 _ -> -- Check for an auth cookie.                   case removeCookie (authConfigCookieName authConfig) (parseCookies headers) of-                    Nothing -> Just <$> redirectToLoginPage base path+                    Nothing -> Just <$> authenticationRequired authConfig base path                     Just (authCookie, cookies) -> do                       auth <- validAuth authConfig authCookie                       case auth of-                        Nothing -> Just <$> redirectToLoginPage base path+                        Nothing -> Just <$> authenticationRequired authConfig base path                         Just token ->                           forwardRequest config send authorize cookies addr request token           forM_ mResponse (sendResponse send)@@ -228,7 +226,9 @@                -> AuthToken                -> IO (Maybe (Response BodyReader)) forwardRequest config send authorize cookies addr request@(Request method path headers _) token = do-    groups <- authorize (authUserEmail . authUser $ token) (maybe (error "No Host") cs $ lookup "Host" headers) path method+    groups <- authorize (authUserEmail . authUser $ token)+                        (fromMaybe (error "No Host") $ lookup "Host" headers)+                        path method     ip <- formatSockAddr addr     case groups of         [] -> Just <$> accessDenied (authUserEmail . authUser $ token)@@ -236,10 +236,10 @@             -- TODO: Reuse connections to the backend server.             let downStreamHeaders =                     toList $-                    insert "From" (cs . authUserEmail . authUser $ token) $-                    insert "X-Groups" (cs $ intercalate "," groups) $-                    insert "X-Given-Name" (cs . authUserGivenName . authUser $ token) $-                    insert "X-Family-Name" (cs . authUserFamilyName . authUser $ token) $+                    insert "From" (B8.pack . authUserEmail . authUser $ token) $+                    insert "X-Groups" (B8.pack $ intercalate "," groups) $+                    insert "X-Given-Name" (B8.pack . authUserGivenName . authUser $ token) $+                    insert "X-Family-Name" (B8.pack . authUserFamilyName . authUser $ token) $                     insert "X-Forwarded-Proto" "https" $                     addForwardedForHeader ip $                     insert "Connection" "close" $