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 +13/−0
- README.md +8/−8
- sproxy.cabal +1/−3
- src/Authenticate.hs +5/−13
- src/Authenticate/Google.hs +3/−2
- src/Authenticate/LinkedIn.hs +3/−2
- src/Authenticate/Types.hs +1/−1
- src/Authorize.hs +9/−9
- src/Main.hs +2/−2
- src/Proxy.hs +10/−10
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" $