wordpress-auth (empty) → 1.0.0.0
raw patch · 6 files changed
+741/−0 lines, 6 filesdep +basedep +base16-bytestringdep +bytestringsetup-changed
Dependencies added: base, base16-bytestring, bytestring, cookie, cryptohash-md5, cryptohash-sha256, hs-php-session, http-types, mtl, text, time, uri-encode
Files
- CHANGELOG.md +5/−0
- LICENSE +30/−0
- README.md +18/−0
- Setup.hs +2/−0
- src/Wordpress/Auth.hs +634/−0
- wordpress-auth.cabal +52/−0
+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# ChangeLog++## v1.0.0.0++Initial Release
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Pavan Rikhi (c) 2018++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Pavan Rikhi nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,18 @@+# wordpress-auth++This package is used for validating Cookie data & Nonces from Wordpress.++You may find it useful if you're trying to serve a Haskell application+alongside a Wordpress site. By validating the Cookies set by Wordpress, you+can access the currently logged-in Wordpress user in Haskell without having+to devise a Wordpress-to-Haskell authentication scheme.++It includes a generalized authentication function, as well as various+helpers, validators, & hashers if you'd like to build a custom+authentication process.++Servant users may want to just use the `servant-auth-wordpress` package.++## License++BSD 3-clause, exceptions possible.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ src/Wordpress/Auth.hs view
@@ -0,0 +1,634 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-| The "Wordpress.Auth" module is used for checking the validity of+various Wordpress authentication schemes.++This is useful if you want a Haskell application to be able to serve+authenticated API requests to a Wordpress site without having to devise+a Wordpress-to-Haskell authentication system.++You will need some constants from your Wordpress site's @wp-config.php@,+like the @NONCE_KEY@ & @NONCE_SALT@, you could supply these via+environmental variables:++@+ loggedInScheme <- 'AuthScheme'+ \<$> ('wpConfigKey' . T.pack \<$> getEnv \"LOGGED_IN_KEY\")+ \<*> ('wpConfigSalt' . T.pack \<$> getEnv \"LOGGED_IN_SALT\")+@++Then you'll want to pull the specific cookie's text out of the Cookie+header(see 'findCookie') & use then 'parseWordpressCookie' to build+a 'WPCookie'. You should then use the `username` field of the cookie to+query your Wordpress database for the User's @ID@('WordpressUserId')+& @user_pass@('WordpresUserPass') fields as well as the @session_tokens@+User Meta('SessionToken').++Equiped with these and the current time(via+'Data.Time.Clock.POSIX.getPOSIXTime'), you can then validate the cookie:++@+ passwordFragment = 'WordpressUserPass' myUserTablesUserPassFieldValue+ sessionTokens = 'decodeSessionTokens' myUserMetaTablesSessionTokensMetaValue+ cookieIsValid = 'validateCookie' loggedInScheme currentTime cookie+ passwordFragment sessionTokens+@++If this is a REST request or a form submission, you should __always__+validate the nonce, even for requests with no auth cookies. The nonce can+be pulled out of the @X-WP-Nonce@ header or the @_wpnonce@ query parameter.++@+ nonceTick <- 'wordpressNonceTick' (60 * 60 * 24) currentTime+ let validNonce = 'validateNonce' nonceScheme (Just cookie) nonceTick+ (Just $ 'WordpressUserId' userId)+ "wp_rest" myNonceText+@++-}+module Wordpress.Auth+ (+ -- * Request Handling+ authorizeWordpressRequest+ , WPAuthConfig(..)+ , UserAuthData(..)+ , WPAuthorization(..)+ , WPAuthError(..)+ , CookieName(..)+ , cookieName+ , findCookie+ , CookieHeaderError(..)+ , findNonce+ -- * Cookies+ , WPCookie(..)+ , CookieToken(..)+ , parseWordpressCookie+ , CookieParseError(..)+ , validateCookie+ , WordpressUserPass(..)+ , CookieValidationError(..)+ , validateCookieHash+ -- * Session Tokens+ , SessionToken(..)+ , decodeSessionTokens+ , validateSessionToken+ -- * Nonces+ , NonceTick(..)+ , wordpressNonceTick+ , validateNonce+ , WordpressUserId(..)+ -- * Hashing / Salting+ , wordpressHash+ , wordpressSalt+ , AuthScheme(..)+ , WordpressKey+ , WordpressSalt+ , wpConfigKey+ , wpConfigSalt+ )+where++import Control.Applicative ( (<|>) )+import Control.Monad ( (<=<)+ , join+ , void+ , unless+ )+import Control.Monad.Except ( MonadIO+ , ExceptT+ , withExceptT+ , runExceptT+ , liftEither+ , liftIO+ , lift+ , throwError+ )+import qualified Crypto.Hash.MD5 as MD5+import qualified Crypto.Hash.SHA256 as SHA256+import qualified Data.ByteString as B+import qualified Data.ByteString.Base16 as Base16+import qualified Data.ByteString.Lazy as LBS+import qualified Data.List as L+import Data.Maybe ( mapMaybe+ , isJust+ )+import Data.PHPSession ( PHPSessionValue(..)+ , decodePHPSessionValue+ )+import qualified Data.Text as T+import Data.Text ( Text )+import Data.Text.Encoding ( encodeUtf8+ , decodeUtf8+ )+import Data.Time.Clock ( NominalDiffTime )+import Data.Time.Clock.POSIX ( POSIXTime+ , getPOSIXTime+ )+import Network.HTTP.Types ( RequestHeaders+ , QueryItem+ )+import qualified Network.URI.Encode as URI+import Text.Read ( readMaybe )+import Web.Cookie ( parseCookiesText )+++-- Request Handling++-- | The is a generalized authentication verification scheme that+-- authorizes a user if the @logged_in@ cookie is set and valid, & verifies+-- the @wp_rest@ nonce action for both authorized & anonymous users.+--+-- The 'WPAuthConfig' failure handler will be used if a Cookie is present+-- but invalid or if the nonce is missing\/invalid.+authorizeWordpressRequest+ :: forall m a+ . MonadIO m+ => WPAuthConfig m a+ -> RequestHeaders+ -> [QueryItem]+ -> m (WPAuthorization a)+authorizeWordpressRequest cfg headers query =+ either (onAuthenticationFailure cfg) return <=< runExceptT $ do+ name <- lift $ getCookieName cfg+ currentTime <- liftIO getPOSIXTime+ either (const $ validateAnonymousUser currentTime)+ (validateAuthorizedUser currentTime)+ $ findCookie name headers+ where+ validateAnonymousUser+ :: POSIXTime -> ExceptT WPAuthError m (WPAuthorization a)+ validateAnonymousUser currentTime =+ checkNonce currentTime Nothing Nothing >> return WPAnonymousUser++ validateAuthorizedUser+ :: POSIXTime -> Text -> ExceptT WPAuthError m (WPAuthorization a)+ validateAuthorizedUser currentTime rawCookie = do+ parsedCookie <- liftWith EParse $ parseWordpressCookie rawCookie+ UserAuthData { userData, wpUser, wpPass, wpTokens } <-+ lift (getUserData cfg $ username parsedCookie)+ >>= liftMaybe UserDataNotFound+ void . liftWith EValid $ validateCookie (loggedInScheme cfg)+ currentTime+ parsedCookie+ wpPass+ wpTokens+ checkNonce currentTime (Just $ token parsedCookie) (Just wpUser)+ return $ WPAuthorizedUser userData++ checkNonce+ :: POSIXTime+ -> Maybe CookieToken+ -> Maybe WordpressUserId+ -> ExceptT WPAuthError m ()+ checkNonce time mToken mUser = do+ nonce <- liftMaybe NoNonce $ findNonce headers query+ let nonceTick = wordpressNonceTick (nonceLifetime cfg) time+ nonceIsValid = validateNonce (nonceScheme cfg)+ mToken+ nonceTick+ mUser+ "wp_rest"+ nonce+ unless nonceIsValid $ throwError InvalidNonce++ liftMaybe :: e -> Maybe b -> ExceptT e m b+ liftMaybe e m = liftEither $ maybe (Left e) Right m++ liftWith :: (e2 -> e1) -> Either e2 b -> ExceptT e1 m b+ liftWith e = withExceptT e . liftEither++-- | The result of the 'authorizeWordpressRequest' function can be an+-- authorized user with some additional data, or an anonymous user.+data WPAuthorization a+ = WPAuthorizedUser a+ | WPAnonymousUser+ deriving (Show, Eq)++-- | Configuration data specific to your Wordpress site & Haskell+-- application.+data WPAuthConfig m a+ = WPAuthConfig+ { getCookieName :: m CookieName+ -- ^ A monadic action that generates a `CookieName`. You can simply+ -- return a constant value, or do something more complex like+ -- querying your database for the `siteurl` option.+ , loggedInScheme :: AuthScheme+ -- ^ The @LOGGED_IN_KEY@ & @LOGGED_IN_SALT@ from your+ -- @wp-config.php@.+ , nonceScheme :: AuthScheme+ -- ^ The @NONCE_KEY@ & @NONCE_SALT@ from your @wp-config.php@.+ , nonceLifetime :: NominalDiffTime+ -- ^ The nonce lifetime of your Wordpress site. Wordpress defaults+ -- to 1 day.+ , getUserData :: Text -> m (Maybe (UserAuthData a))+ -- ^ A function to pull your custom data & the user data needed for+ -- authentication. See the 'UserAuthData' type.+ , onAuthenticationFailure :: WPAuthError -> m (WPAuthorization a)+ -- ^ How to handle authentication failures. You might want to throw+ -- an HTTP error or simply treat the user as unauthenticated.+ }++-- | The data needed for authentication, along with some arbitrary data+-- that is returned on success.+data UserAuthData a =+ UserAuthData+ { userData :: a+ -- ^ Arbitrary data that the validation should return. E.g., if you+ -- query your users table for the @ID@ & @user_pass@, you can+ -- return your whole User type so you don't have to make another+ -- database call in your handler.+ , wpUser :: WordpressUserId+ -- ^ The @ID@ field of the User.+ , wpPass :: WordpressUserPass+ -- ^ The @user_pass@ field of the User.+ , wpTokens :: [SessionToken]+ -- ^ The @session_tokens@ usermeta for the User. You can use+ -- 'Wordpress.Auth.decodeSessionTokens' to parse the raw meta value.+ }+ deriving (Show, Eq)++-- | Potential errors during authentication.+data WPAuthError+ = EHeader CookieHeaderError+ -- ^ Header Error.+ | EParse CookieParseError+ -- ^ Parsing Error.+ | EValid CookieValidationError+ -- ^ Validation Error.+ | UserDataNotFound+ -- ^ The `getUserData` function returned `Nothing`.+ | NoNonce+ -- ^ The `Request` has no @X-WP-Nonce@ header.+ | InvalidNonce+ -- ^ The nonce couldn't be validated.+ deriving (Show, Eq)+++-- | The name of a Wordpress authentication cookie. Wordpress's frontend+-- uses @CookieNameWithMD5 "wordpress_logged_in_" "\<your-site-url>"@ by+-- default.+data CookieName+ = CustomCookieName Text+ -- ^ A constant name for the cookie.+ | CookieNameWithMD5 Text Text+ -- ^ A cookie name with some text to hash & append. E.g., Wordpress's+ -- @logged_in@ auth scheme uses @wordpress_logged_in_@ suffixed with+ -- the MD5 hash of the @siteurl@ option.+ deriving (Show, Eq)++-- | Build the name of an authentication cookie from a `CookieName`,+-- hashing the suffix if present.+cookieName :: CookieName -> Text+cookieName = \case+ CustomCookieName n -> n+ CookieNameWithMD5 name textToHash ->+ name <> hashText MD5.hash (HashMessage textToHash)++-- | Try to find & decode a Cookie in the headers with the given name.+findCookie :: CookieName -> RequestHeaders -> Either CookieHeaderError Text+findCookie name headers = do+ header <- liftMaybe NoCookieHeader $ lookup "cookie" headers+ let cookieBody = parseCookiesText header+ authCookie = URI.decodeText <$> lookup (cookieName name) cookieBody+ liftMaybe NoCookieMatches authCookie+ where liftMaybe e = maybe (Left e) Right++-- | Potential errors while searching for a specific cookie in the request+-- headers.+data CookieHeaderError+ = NoCookieHeader+ -- ^ The `Request` has no `Cookie` header.+ | NoCookieMatches+ -- ^ No Cookie matched the expected `CookieName`.+ deriving (Show, Eq)++-- | Try to find & decode a Nonce in either the @X-WP-Nonce@ header or the+-- @_wpnonce@ query parameter.+findNonce :: RequestHeaders -> [QueryItem] -> Maybe Text+findNonce headers query =+ fmap decodeUtf8 $ lookup "x-wp-nonce" headers <|> join+ (lookup "_wpnonce" query)+++-- Cookies++-- | This represents a Cookie set by a Wordpress authentication scheme+-- (`auth`, `auth_sec`, & `logged_in`).+data WPCookie+ = WPCookie+ { username :: Text+ -- ^ The `user_login` column for the Wordpress User.+ , expiration :: POSIXTime+ -- ^ The expiration time of the Cookie.+ , token :: CookieToken+ -- ^ The Wordpress User's session token.+ , hmac :: Text+ -- ^ A SHA256 HMAC hash of the token & some user data.+ }+ deriving (Show, Eq)++-- | A User's Wordpress Session Token from an auth cookie.+newtype CookieToken+ = CookieToken { cookieToken :: Text }+ deriving (Show, Eq)++-- | Potential errors we may encounter while parsing a `WPCookie`.+data CookieParseError+ = MalformedCookie+ -- ^ The cookie did not have 4 fields separated by `|` characters.+ | InvalidExpiration+ -- ^ The `expiration` field of the cookie is not an Integer.+ deriving (Show, Eq)++-- | Parse a `WPCookie` from the body text of an `auth`, `auth_sec`, or+-- `logged_in` cookie.+parseWordpressCookie :: Text -> Either CookieParseError WPCookie+parseWordpressCookie rawCookie = case T.splitOn "|" rawCookie of+ [username, expiration_, token_, hmac] ->+ let token = CookieToken token_+ in case fromInteger <$> readMaybe (T.unpack expiration_) of+ Just expiration -> Right WPCookie { .. }+ Nothing -> Left InvalidExpiration+ _ -> Left MalformedCookie++-- | The @ID@ field from the @users@ table of a Wordpress site.+newtype WordpressUserId+ = WordpressUserId { wordpressUserId :: Integer }+ deriving (Show, Eq)++-- | The @user_pass@ field from the @users@ table of a Wordpress site.+newtype WordpressUserPass+ = WordpressUserPass { wordpressUserPass :: Text }+ deriving (Show, Eq)++-- | Determine if a `WPCookie`'s hash matches the hashed password & token.+--+-- A secret is generated by hashing the user, password, expiration, & token.+-- This secret is then used to hash the user, expiration, & token. The+-- resulting hash should match the `hmac` hash in the `WPCookie`.+validateCookieHash :: AuthScheme -> WPCookie -> WordpressUserPass -> Bool+validateCookieHash scheme cookie userPass =+ let+ passwordFragment = T.take 4 $ T.drop 8 $ wordpressUserPass userPass+ user = username cookie+ tok = cookieToken $ token cookie+ secret = wordpressHash scheme $ joinHashParts+ [user, passwordFragment, posixText $ expiration cookie, tok]+ hash =+ hmacText SHA256.hmac (HashSecret secret)+ $ HashMessage+ $ joinHashParts [user, posixText $ expiration cookie, tok]+ in+ hash == hmac cookie+ where+ posixText :: POSIXTime -> Text+ posixText t = T.pack $ show (floor t :: Integer)++-- | Validate a Wordpress Authentication Cookie by verifying that the hash+-- & token in the cookie are valid and the expiration time is in the+-- future.+validateCookie+ :: AuthScheme -- ^ They @_KEY@ & @_SALT@ constants for a WP auth scheme.+ -> POSIXTime -- ^ The current time.+ -> WPCookie -- ^ The cookie to validate.+ -> WordpressUserPass -- ^ The @user_pass@ field for the cookie's user.+ -> [SessionToken] -- ^ The @session_tokens@ meta for the cookie's user.+ -> Either CookieValidationError ()+validateCookie scheme currentTime cookie userPass sessionTokens =+ let validHash = validateCookieHash scheme cookie userPass+ validSessionToken =+ validateSessionToken currentTime (token cookie) sessionTokens+ in if currentTime > expiration cookie+ then Left CookieExpired+ else case (validHash, validSessionToken) of+ (False, _ ) -> Left InvalidHash+ (_ , False) -> Left InvalidToken+ (True , True ) -> Right ()+++-- | Potential validation errors for a `WPCookie`.+data CookieValidationError+ = CookieExpired+ -- ^ The `expiration` time of the cookie is in the past.+ | InvalidHash+ -- ^ The `hmac` hash in the cookie doesn't match the calculated hash.+ | InvalidToken+ -- ^ The `token` in the cookie is not valid or expired.+ deriving (Show, Eq)+++++-- Hashing / Salting++-- | A port of the `wp_hash` function. This performs an 'MD5.hmac' hash on+-- some text using a secret derived from the authentication scheme's key+-- & salt constants.+wordpressHash :: AuthScheme -> Text -> Text+wordpressHash scheme textToHash =+ let secret = HashSecret $ wordpressSalt scheme+ in hmacText MD5.hmac secret $ HashMessage textToHash++-- | A port of the `wp_salt` function. Builds a secret key for a hashing+-- function using the auth scheme's key & salt.+wordpressSalt :: AuthScheme -> Text+wordpressSalt AuthScheme { schemeKey, schemeSalt } =+ unKey schemeKey <> unSalt schemeSalt++++-- Tokens++-- | A User Session's Token. These can be found in the `usermeta` Wordpress+-- table for rows where @meta_key="session_token"@.+--+-- You'll probably want to use `decodeSessionTokens` to parse the tables's+-- @meta_value@ instead of constructing them yourself.+data SessionToken+ = SessionToken+ { sessionToken :: Text+ , tokenExpiration :: POSIXTime+ }+ deriving (Show, Eq)++-- | Decode a serialized PHP array containing a User's Session Tokens.+-- These are usually stored as the `session_tokens` usermeta.+--+-- It may be an associative array of tokens to expiration times, or tokens+-- to an associative array of sub-fields:+--+-- > array(+-- > 'some-random-hex-text' => 192836504,+-- > // ...+-- > );+-- > array(+-- > 'deadbeef ' => array(+-- > 'expiration' => 9001,+-- > // ...+-- > ),+-- > );+decodeSessionTokens :: Text -> [SessionToken]+decodeSessionTokens serializedText =+ case decodePHPSessionValue (LBS.fromStrict $ encodeUtf8 serializedText) of+ Nothing -> []+ Just phpValue -> decodeTokenArray phpValue+ where+ decodeTokenArray :: PHPSessionValue -> [SessionToken]+ decodeTokenArray = \case+ PHPSessionValueArray sessionTokens ->+ mapMaybe decodeToken sessionTokens+ _ -> []+ -- Decode a single Token, which can be a (token, expiration) pair, or+ -- an associative array.+ decodeToken :: (PHPSessionValue, PHPSessionValue) -> Maybe SessionToken+ decodeToken = \case+ (PHPSessionValueString token, expirationData) ->+ let decodedExpiration = case expirationData of+ PHPSessionValueInt posixExpiration ->+ Just . fromInteger $ fromIntegral posixExpiration+ PHPSessionValueArray tokenData -> decodeTokenData tokenData+ _ -> Nothing+ sessionToken = decodeUtf8 $ LBS.toStrict token+ in (\tokenExpiration -> SessionToken { .. }) <$> decodedExpiration++ _ -> Nothing+ -- Decode the sub-fields of a Token.+ decodeTokenData :: [(PHPSessionValue, PHPSessionValue)] -> Maybe POSIXTime+ decodeTokenData = \case+ [] -> Nothing+ (PHPSessionValueString "expiration", PHPSessionValueInt expiration) : _+ -> Just $ fromInteger $ fromIntegral expiration+ _ : rest -> decodeTokenData rest+++-- | Determine if the SHA256 hash of the token matches one of the unexpired+-- session tokens.+validateSessionToken+ :: POSIXTime -- ^ The current time+ -> CookieToken -- ^ The session token from a `WPCookie`+ -> [SessionToken] -- ^ A list of the User's session tokens+ -> Bool+validateSessionToken currentTime (CookieToken cookieToken) sessionTokens =+ let hashedCookieToken = hashText SHA256.hash $ HashMessage cookieToken+ in isJust $ L.find ((== hashedCookieToken) . sessionToken) $ filter+ (\tok -> tokenExpiration tok >= currentTime)+ sessionTokens++++-- Nonces++-- | The tick number of a Wordpress site - required for Nonce verification.+newtype NonceTick+ = NonceTick+ { tickCount :: Integer+ }+ deriving (Show, Eq)++-- | A port of the @wp_nonce_tick@ function. Calculates the nonce tick+-- number, where each nonce has a lifetime of two ticks.+wordpressNonceTick+ :: NominalDiffTime -- ^ The nonce lifetime. Wordpress's default is 1 day.+ -> POSIXTime -- ^ The current time.+ -> NonceTick+wordpressNonceTick nonceLifetime currentTime =+ let currentTick = toRational currentTime / (toRational nonceLifetime / 2)+ in NonceTick $ ceiling currentTick++-- | Determine if the tick-dependent hash of the `CookieToken` matches the+-- hash of the current or previous tick.+validateNonce+ :: AuthScheme -- ^ The Wordpress site's @nonce@ scheme constants - @NONCE_KEY@ & @NONCE_SALT@.+ -> Maybe CookieToken -- ^ A token from the `logged_in` cookie.+ -> NonceTick -- ^ The current tick number.+ -> Maybe WordpressUserId -- ^ The ID of the currently logged in User.+ -> Text -- ^ The @action@ of the nonce (e.g., @"wp_rest"@ for API requests).+ -> Text -- ^ The nonce to verify.+ -> Bool+validateNonce scheme maybeToken tick maybeUserId action nonce =+ let+ userId = maybe "" (T.pack . show . wordpressUserId) maybeUserId+ token = maybe "" cookieToken maybeToken+ thisCycleHash = hashAndTrim $ joinHashParts+ [T.pack $ show $ tickCount tick, action, userId, token]+ lastCycleHash = hashAndTrim $ joinHashParts+ [T.pack $ show $ tickCount tick - 1, action, userId, token]+ in+ nonce /= "" && nonce `elem` [thisCycleHash, lastCycleHash]+ where+ hashAndTrim s =+ let hashed = wordpressHash scheme s+ in T.take 10 $ T.drop (T.length hashed - 12) hashed++++-- Basic Types++-- | This represents one of the @$scheme@s that Wordpress's cookie/nonce+-- functions use to salt their hashes.+--+-- The built-in Wordpress schemes are @auth@\/@auth_sec@ for HTTP\/HTTPS+-- requests to @/wp-admin/@, @logged_in@ for authenticated front-end+-- requests, & @nonce@ for form submissions & API requests.+--+-- The secret keys & salts are constants found in your @wp-config.php@+-- file, defined as @LOGGED_IN_SALT@, @LOGGED_IN_KEY@, etc.+--+data AuthScheme+ = AuthScheme+ { schemeKey :: WordpressKey+ , schemeSalt :: WordpressSalt+ }+ deriving (Show, Eq)++-- | An auth scheme's `_KEY` constant, usually defined in your Wordpress+-- site's @wp-config.php@. E.g., `LOGGED_IN_KEY`+newtype WordpressKey+ = WordpressKey { unKey :: Text }+ deriving (Show, Eq)+-- | An auth scheme's `_SALT` constant, usually defined in your Wordpress+-- site's @wp-config.php@. E.g., `LOGGED_IN_SALT`+newtype WordpressSalt+ = WordpressSalt { unSalt :: Text }+ deriving (Show, Eq)++-- | Build the `_KEY` value for an authentiation scheme.+wpConfigKey :: Text -> WordpressKey+wpConfigKey = WordpressKey++-- | Build the `_SALT` value for an authentiation scheme.+wpConfigSalt :: Text -> WordpressSalt+wpConfigSalt = WordpressSalt++++-- Helpers++newtype HashSecret = HashSecret Text+newtype HashMessage = HashMessage { hashMessage :: Text }+-- Apply an HMAC hashing function, converting the output to+-- a hex-representation.+hmacText+ :: (B.ByteString -> B.ByteString -> B.ByteString)+ -> HashSecret+ -> HashMessage+ -> Text+hmacText hasher (HashSecret secret) =+ decodeUtf8+ . Base16.encode+ . hasher (encodeUtf8 secret)+ . encodeUtf8+ . hashMessage++-- Apply a hashing function to Text values, converting the output to+-- a hex-representation.+hashText :: (B.ByteString -> B.ByteString) -> HashMessage -> Text+hashText hasher =+ decodeUtf8 . Base16.encode . hasher . encodeUtf8 . hashMessage++-- Join the different text to hash together by `|` like Wordpress expects.+joinHashParts :: [Text] -> Text+joinHashParts = T.intercalate "|"
+ wordpress-auth.cabal view
@@ -0,0 +1,52 @@+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.31.1.+--+-- see: https://github.com/sol/hpack+--+-- hash: c33e1a894c3fedf7883654d7bd9f0199378cdece3892f962cd629ba877ddaba9++name: wordpress-auth+version: 1.0.0.0+synopsis: Validate Wordpress Cookies & Nonces; Build Wordpress Hashes & Salts+description: This package is used for validating Cookie data & Nonces from Wordpress.+ .+ You may find it useful if you're trying to serve a Haskell application alongside a Wordpress site. By validating the Cookies set by Wordpress, you can access the currently logged-in Wordpress user in Haskell without having to devise a Wordpress-to-Haskell authentication scheme.+ .+ It includes a generalized authentication function, as well as various helpers, validators, & hashers if you'd like to build a custom authentication process.+ .+ Servant users may want to just use the @servant-auth-wordpress@ package.+category: Web+homepage: https://github.com/prikhi/wordpress-auth#readme+author: Pavan Rikhi+maintainer: pavan.rikhi@gmail.com+copyright: BSD3+license: BSD3+license-file: LICENSE+build-type: Simple+extra-source-files:+ README.md+ CHANGELOG.md++library+ exposed-modules:+ Wordpress.Auth+ other-modules:+ Paths_wordpress_auth+ hs-source-dirs:+ src+ ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints+ build-depends:+ base >=4.7 && <5+ , base16-bytestring >=0.1 && <0.2+ , bytestring >=0.10 && <0.12+ , cookie >=0.4 && <0.5+ , cryptohash-md5 >=0.11 && <0.12+ , cryptohash-sha256 >=0.11 && <0.12+ , hs-php-session >=0.0.9 && <0.1+ , http-types >=0.12 && <0.13+ , mtl >=2 && <3+ , text >=1 && <2+ , time >=1 && <2+ , uri-encode >=1.5 && <1.6+ default-language: Haskell2010