packages feed

wai-middleware-auth 0.2.3.1 → 0.2.4.1

raw patch · 5 files changed

+202/−8 lines, 5 filesdep +optparse-applicativePVP ok

version bump matches the API change (PVP)

Dependencies added: optparse-applicative

API changes (from Hackage documentation)

+ Network.Wai.Middleware.Auth.OAuth2.Gitlab: Gitlab :: Text -> Text -> [ByteString] -> OAuth2 -> Gitlab
+ Network.Wai.Middleware.Auth.OAuth2.Gitlab: [gitlabAPIUserEndpoint] :: Gitlab -> Text
+ Network.Wai.Middleware.Auth.OAuth2.Gitlab: [gitlabAppName] :: Gitlab -> Text
+ Network.Wai.Middleware.Auth.OAuth2.Gitlab: [gitlabEmailWhitelist] :: Gitlab -> [ByteString]
+ Network.Wai.Middleware.Auth.OAuth2.Gitlab: [gitlabOAuth2] :: Gitlab -> OAuth2
+ Network.Wai.Middleware.Auth.OAuth2.Gitlab: data Gitlab
+ Network.Wai.Middleware.Auth.OAuth2.Gitlab: gitlabParser :: ProviderParser
+ Network.Wai.Middleware.Auth.OAuth2.Gitlab: instance Data.Aeson.Types.FromJSON.FromJSON Network.Wai.Middleware.Auth.OAuth2.Gitlab.Gitlab
+ Network.Wai.Middleware.Auth.OAuth2.Gitlab: instance Data.Aeson.Types.FromJSON.FromJSON Network.Wai.Middleware.Auth.OAuth2.Gitlab.GitlabEmail
+ Network.Wai.Middleware.Auth.OAuth2.Gitlab: instance GHC.Show.Show Network.Wai.Middleware.Auth.OAuth2.Gitlab.GitlabEmail
+ Network.Wai.Middleware.Auth.OAuth2.Gitlab: instance Network.Wai.Middleware.Auth.Provider.AuthProvider Network.Wai.Middleware.Auth.OAuth2.Gitlab.Gitlab
+ Network.Wai.Middleware.Auth.OAuth2.Gitlab: mkGitlabProvider :: Text -> Text -> Text -> Text -> [ByteString] -> Maybe ProviderInfo -> Gitlab

Files

CHANGELOG.md view
@@ -1,3 +1,7 @@+# 0.2.4.0++- Add GitLab provider.+ # 0.2.3.1  - Expose `discoverURI` in `Network.Wai.Middleware.Auth.OIDC`
README.md view
@@ -22,8 +22,8 @@ Google and Github.  Configuration is done using a yaml config file. Here is a sample file that will-configure `wai-auth` to run a file server with google and github authentication-on `http://localhost:3000`:+configure `wai-auth` to run a file server with Google, GitHub, and GitLab+authentication on `http://localhost:3000`:  ```yaml app_root: "_env:APPROOT:http://localhost:3000"@@ -46,6 +46,12 @@     client_secret: "...oxW"     email_white_list:       - "^[a-zA-Z0-9._%+-]+@example.com$"+  gitlab:+    client_id: "...9cfc"+    client_secret: "...f0d0"+    app_name: "Dev App for wai-middleware-auth"+    email_white_list:+      - "^[a-zA-Z0-9._%+-]+@example.com$" ```  Above configuration will also block access to users that don't have an email@@ -57,8 +63,9 @@ azuCFq0zEBkLSXhQrhliZzZD8Kblo... ``` -Make sure you have proper callback/redirect urls registered with google/github-apps, eg: `http://localhost:3000/_auth_middleware/google/complete`.+Make sure you have proper callback/redirect urls registered with+google/github/gitlab apps, eg:+`http://localhost:3000/_auth_middleware/google/complete`.  After configuration file is ready, running application is very easy: @@ -67,3 +74,34 @@ Listening on port 3000 ``` +### Reverse proxy++To use a reverse proxy instead of a file server, replace `file_server` with+`reverse_proxy`, eg:++```yaml+reverse_proxy:+  host: myapp.example.com+  port: 80+```++### Self-hosted GitLab++The GitLab provider also supports using a self-hosted GitLab instance by+setting the `gitlab_host` field.  In this case you may also want to override+the `provider_info` to change the title, logo, and description.  For example:++```yaml+providers:+  gitlab:+    gitlab_host: gitlab.mycompany.com+    client_id: "...9cfc"+    client_secret: "...f0d0"+    app_name: "Dev App for wai-middleware-auth"+    email_white_list:+      - "^[a-zA-Z0-9._%+-]+@mycompany.com$"+    provider_info:+      title: My Company's GitLab+      logo_url: https://mycompany.com/logo.png+      descr: Use your My Company GitLab account to access this page.+```
app/Main.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards   #-} {-# LANGUAGE TemplateHaskell   #-}@@ -8,6 +9,7 @@ import           Network.Wai.Handler.Warp                  (run) import           Network.Wai.Middleware.Auth import           Network.Wai.Middleware.Auth.OAuth2+import           Network.Wai.Middleware.Auth.OAuth2.Gitlab import           Network.Wai.Middleware.Auth.OAuth2.Github import           Network.Wai.Middleware.Auth.OAuth2.Google import           Network.Wai.Middleware.RequestLogger      (logStdout)@@ -18,6 +20,14 @@   = ConfigFile FilePath   | KeyFile KeyOptions +showHelpText :: ParseError+showHelpText = ShowHelpText+#if MIN_VERSION_optparse_applicative(0,16,0)+                Nothing+#endif+++ basicSettingsParser :: String -> Parser BasicOptions basicSettingsParser version =   (ConfigFile <$>@@ -28,7 +38,7 @@      (InfoMsg version)      (long "version" <> short 'v' <> help "Current version.") <*    abortOption-     ShowHelpText+     showHelpText      (long "help" <> short 'h' <> help "Display this message.")) <|>   (subparser      (command@@ -60,7 +70,7 @@     (long "base64" <> short 'b' <>      help "Produce a key in a base64 encoded form.") <*   abortOption-    ShowHelpText+    showHelpText     (long "help" <> short 'h' <> help "Display this message.")  @@ -76,7 +86,7 @@   case opts of     ConfigFile configFile -> do       authConfig <- readAuthConfig configFile-      mkMain authConfig [githubParser, googleParser, oAuth2Parser] $ \port app -> do+      mkMain authConfig [gitlabParser, githubParser, googleParser, oAuth2Parser] $ \port app -> do         putStrLn $ "Listening on port " ++ show port         run port $ logStdout app     KeyFile (KeyOptions {..}) -> do
+ src/Network/Wai/Middleware/Auth/OAuth2/Gitlab.hs view
@@ -0,0 +1,140 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards   #-}+module Network.Wai.Middleware.Auth.OAuth2.Gitlab+    ( Gitlab(..)+    , mkGitlabProvider+    , gitlabParser+    ) where+import           Control.Exception.Safe               (catchAny)+import           Data.Maybe                           (fromMaybe)+import           Data.Aeson+import qualified Data.ByteString                      as S+import           Data.Proxy                           (Proxy (..))+import qualified Data.Text                            as T+import           Data.Text.Encoding                   (encodeUtf8)+import           Network.HTTP.Simple                  (getResponseBody,+                                                       httpJSON, parseRequest,+                                                       setRequestHeaders)+import           Network.HTTP.Types+import qualified Network.OAuth.OAuth2                 as OA2+import           Network.Wai.Auth.Internal            (decodeToken)+import           Network.Wai.Auth.Tools               (getValidEmail)+import           Network.Wai.Middleware.Auth.OAuth2+import           Network.Wai.Middleware.Auth.Provider++-- | Create a gitlab authentication provider+--+-- @since 0.2.4.0+mkGitlabProvider+  :: T.Text -- ^ Hostname of GitLab instance (e.g. @gitlab.com@)+  -> T.Text -- ^ Name of the application as it is registered on gitlab+  -> T.Text -- ^ @client_id@ from gitlab+  -> T.Text -- ^ @client_secret@ from gitlab+  -> [S.ByteString] -- ^ White list of posix regular expressions for emails+  -- attached to gitlab account.+  -> Maybe ProviderInfo -- ^ Replacement for default info+  -> Gitlab+mkGitlabProvider gitlabHost appName clientId clientSecret emailWhiteList mProviderInfo =+  Gitlab+    appName+    ("https://" <> gitlabHost <> "/api/v4/user")+    emailWhiteList+    OAuth2+    { oa2ClientId = clientId+    , oa2ClientSecret = clientSecret+    , oa2AuthorizeEndpoint = ("https://" <> gitlabHost <> "/oauth/authorize")+    , oa2AccessTokenEndpoint = ("https://" <> gitlabHost <> "/oauth/token")+    , oa2Scope = Just ["read_user"]+    , oa2ProviderInfo = fromMaybe defProviderInfo mProviderInfo+    }+  where+    defProviderInfo =+      ProviderInfo+      { providerTitle = "GitLab"+      , providerLogoUrl =+          "https://about.gitlab.com/images/press/logo/png/gitlab-icon-rgb.png"+      , providerDescr = "Use your GitLab account to access this page."+      }++-- | Aeson parser for `Gitlab` provider.+--+-- @since 0.2.4.0+gitlabParser :: ProviderParser+gitlabParser = mkProviderParser (Proxy :: Proxy Gitlab)+++-- | Gitlab authentication provider+data Gitlab = Gitlab+  { gitlabAppName          :: T.Text+  , gitlabAPIUserEndpoint  :: T.Text+  , gitlabEmailWhitelist   :: [S.ByteString]+  , gitlabOAuth2           :: OAuth2+  }++instance FromJSON Gitlab where+  parseJSON =+    withObject "Gitlab Provider Object" $ \obj -> do+      gitlabHost <- obj .:? "gitlab_host"+      appName <- obj .: "app_name"+      clientId <- obj .: "client_id"+      clientSecret <- obj .: "client_secret"+      emailWhiteList <- obj .:? "email_white_list" .!= []+      mProviderInfo <- obj .:? "provider_info"+      return $+        mkGitlabProvider+          (fromMaybe "gitlab.com" gitlabHost)+          appName+          clientId+          clientSecret+          (map encodeUtf8 emailWhiteList)+          mProviderInfo++-- | Newtype wrapper for a gitlab user+newtype GitlabEmail = GitlabEmail { gitlabEmail :: S.ByteString } deriving Show++instance FromJSON GitlabEmail where+  parseJSON = withObject "Gitlab Email" $ \ obj -> do+    email <- obj .: "email"+    return (GitlabEmail $ encodeUtf8 email)+++-- | Makes an API call to gitlab and retrieves user's verified email.+-- Note: we only retrieve the PRIMARY email, because there is no way+-- to tell whether secondary emails are verified or not.+retrieveUser :: T.Text -> T.Text -> S.ByteString -> IO GitlabEmail+retrieveUser appName userApiEndpoint accessToken = do+  req <- parseRequest (T.unpack userApiEndpoint)+  resp <- httpJSON $ setRequestHeaders headers req+  return $ getResponseBody resp+  where+    headers =+      [ ("Authorization", "Bearer " <> accessToken)+      , ("User-Agent", encodeUtf8 appName)+      ]+++instance AuthProvider Gitlab where+  getProviderName _ = "gitlab"+  getProviderInfo = getProviderInfo . gitlabOAuth2+  handleLogin Gitlab {..} req suffix renderUrl onSuccess onFailure = do+    let onOAuth2Success oauth2Tokens = do+          catchAny+            (do accessToken <-+                  case decodeToken oauth2Tokens of+                    Left err -> fail err+                    Right tokens -> pure $ encodeUtf8 $ OA2.atoken $ OA2.accessToken tokens+                email <-+                  gitlabEmail <$>+                  retrieveUser+                    gitlabAppName+                    gitlabAPIUserEndpoint+                    accessToken+                let mValidEmail = getValidEmail gitlabEmailWhitelist [email]+                case mValidEmail of+                  Just validEmail -> onSuccess validEmail+                  Nothing ->+                    onFailure status403 $+                    "Your primary email address does not have permission to access this resource. " <>+                    "Please contact the administrator.")+            (\_err -> onFailure status501 "Issue communicating with gitlab")+    handleLogin gitlabOAuth2 req suffix renderUrl onOAuth2Success onFailure
wai-middleware-auth.cabal view
@@ -1,6 +1,6 @@ cabal-version:       1.18 name:                wai-middleware-auth-version:             0.2.3.1+version:             0.2.4.1 synopsis:            Authentication middleware that secures WAI application description:         Please see the README and Haddocks at <https://www.stackage.org/package/wai-middleware-auth> license:             MIT@@ -14,6 +14,7 @@ library   exposed-modules:     Network.Wai.Middleware.Auth                        Network.Wai.Middleware.Auth.OAuth2+                       Network.Wai.Middleware.Auth.OAuth2.Gitlab                        Network.Wai.Middleware.Auth.OAuth2.Github                        Network.Wai.Middleware.Auth.OAuth2.Google                        Network.Wai.Middleware.Auth.OIDC@@ -72,6 +73,7 @@                      , cereal                      , clientsession                      , optparse-simple+                     , optparse-applicative                      , wai-extra                      , wai-middleware-auth                      , warp