diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# ChangeLog
+
+## v1.0.0.0
+
+Initial Release
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -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.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -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.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/src/Wordpress/Auth.hs b/src/Wordpress/Auth.hs
new file mode 100644
--- /dev/null
+++ b/src/Wordpress/Auth.hs
@@ -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 "|"
diff --git a/wordpress-auth.cabal b/wordpress-auth.cabal
new file mode 100644
--- /dev/null
+++ b/wordpress-auth.cabal
@@ -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
