diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,25 @@
+The following license covers this documentation, and the source code, except
+where otherwise indicated.
+
+Copyright 2008, Michael Snoyman. 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.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "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 HOLDERS 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/Setup.lhs b/Setup.lhs
new file mode 100644
--- /dev/null
+++ b/Setup.lhs
@@ -0,0 +1,7 @@
+#!/usr/bin/env runhaskell
+
+> module Main where
+> import Distribution.Simple
+
+> main :: IO ()
+> main = defaultMain
diff --git a/Web/Authenticate/OpenId.hs b/Web/Authenticate/OpenId.hs
new file mode 100644
--- /dev/null
+++ b/Web/Authenticate/OpenId.hs
@@ -0,0 +1,147 @@
+{-# LANGUAGE FlexibleInstances #-}
+---------------------------------------------------------
+-- |
+-- Module        : Web.Authenticate.OpenId
+-- Copyright     : Michael Snoyman
+-- License       : BSD3
+--
+-- Maintainer    : Michael Snoyman <michael@snoyman.com>
+-- Stability     : Unstable
+-- Portability   : portable
+--
+-- Provides functionality for being an OpenId consumer.
+--
+---------------------------------------------------------
+module Web.Authenticate.OpenId
+    ( Identifier (..)
+    , getForwardUrl
+    , authenticate
+    ) where
+
+import Data.Maybe (fromMaybe, fromJust)
+import Network.HTTP.Wget
+import Text.HTML.TagSoup
+import Numeric (showHex)
+
+-- | An openid identifier (ie, a URL).
+data Identifier = Identifier { identifier :: String }
+
+instance Monad (Either String) where
+    return = Right
+    fail = Left
+    (Left s) >>= _ = Left s
+    (Right x) >>= f = f x
+
+-- | Returns a URL to forward the user to in order to login.
+getForwardUrl :: Monad m
+              => String -- ^ The openid the user provided.
+              -> String -- ^ The URL for this application\'s complete page.
+              -> IO (m String) -- ^ URL to send the user to.
+getForwardUrl openid complete = do
+    bodyIdent' <- wget openid [] []
+    case bodyIdent' of
+        Left s -> return $ fail s
+        Right bodyIdent -> do
+        server <- getOpenIdVar "server" bodyIdent
+        let delegate = fromMaybe openid $ getOpenIdVar "delegate" bodyIdent
+        return $ return $ constructUrl server
+                            [ ("openid.mode", "checkid_setup")
+                            , ("openid.identity", delegate)
+                            , ("openid.return_to", complete)
+                            ]
+
+getOpenIdVar :: Monad m => String -> String -> m String
+getOpenIdVar var content = do
+    let tags = parseTags content
+    let secs = sections (~== ("<link rel=openid." ++ var ++ ">")) tags
+    secs' <- mhead secs
+    secs'' <- mhead secs'
+    return $ fromAttrib "href" secs''
+    where
+        mhead [] = fail $ "Variable not found: openid." ++ var
+        mhead (x:_) = return x
+
+constructUrl :: String -> [(String, String)] -> String
+constructUrl url [] = url
+constructUrl url args = url ++ "?" ++ queryString args
+    where
+        queryString [] = error "queryString with empty args cannot happen"
+        queryString [first] = onePair first
+        queryString (first:rest) = onePair first ++ "&" ++ queryString rest
+        onePair (x, y) = urlEncode x ++ "=" ++ urlEncode y
+
+-- | Handle a redirect from an OpenID provider and check that the user
+-- logged in properly. If it was successfully, 'return's the openid.
+-- Otherwise, 'fail's an explanation.
+authenticate :: Monad m => [(String, String)] -> IO (m Identifier)
+authenticate req = do -- FIXME check openid.mode == id_res (not cancel)
+    authUrl' <- getAuthUrl req
+    case authUrl' of
+        Nothing -> return $ fail "Invalid parameters"
+        Just authUrl -> do
+            content' <- wget authUrl [] []
+            case content' of
+                Left s -> return $ fail s
+                Right content -> do
+                    let isValid = contains "is_valid:true" content
+                    if isValid
+                        then return $
+                             return $ Identifier
+                             (fromJust $ lookup "openid.identity" req)
+                        else return $ fail content
+
+getAuthUrl :: [(String, String)] -> IO (Maybe String)
+getAuthUrl req = do
+    let identity' = lookup "openid.identity" req
+    case identity' of
+        Nothing -> return Nothing
+        Just identity -> do
+            idContent <- wget identity [] []
+            case idContent of
+                Nothing -> return Nothing
+                Just x -> return $ helper x
+    where
+        helper :: String -> Maybe String
+        helper idContent = do
+            server <- getOpenIdVar "server" idContent
+            dargs <- mapM makeArg [
+                "assoc_handle",
+                "sig",
+                "signed",
+                "identity",
+                "return_to"
+                ]
+            let sargs = [("openid.mode", "check_authentication")]
+            return $ constructUrl server $ dargs ++ sargs
+        makeArg :: String -> Maybe (String, String)
+        makeArg s = do
+            let k = "openid." ++ s
+            v <- lookup k req
+            return (k, v)
+
+contains :: String -> String -> Bool
+contains [] _ = True
+contains _ [] = False
+contains needle haystack =
+    begins needle haystack ||
+    (contains needle $ tail haystack)
+
+begins :: String -> String -> Bool
+begins [] _ = True
+begins _ [] = False
+begins (x:xs) (y:ys) = x == y && begins xs ys
+
+urlEncode :: String -> String
+urlEncode = concatMap urlEncodeChar
+
+urlEncodeChar :: Char -> String
+urlEncodeChar x
+    | safeChar (fromEnum x) = return x
+    | otherwise = '%' : showHex (fromEnum x) ""
+
+safeChar :: Int -> Bool
+safeChar x
+    | x >= fromEnum 'a' && x <= fromEnum 'z' = True
+    | x >= fromEnum 'A' && x <= fromEnum 'Z' = True
+    | x >= fromEnum '0' && x <= fromEnum '9' = True
+    | otherwise = False
diff --git a/Web/Authenticate/Rpxnow.hs b/Web/Authenticate/Rpxnow.hs
new file mode 100644
--- /dev/null
+++ b/Web/Authenticate/Rpxnow.hs
@@ -0,0 +1,71 @@
+---------------------------------------------------------
+--
+-- Module        : Web.Authenticate.Rpxnow
+-- Copyright     : Michael Snoyman
+-- License       : BSD3
+--
+-- Maintainer    : Michael Snoyman <michael@snoyman.com>
+-- Stability     : Unstable
+-- Portability   : portable
+--
+-- Facilitates authentication with "http://rpxnow.com/".
+--
+---------------------------------------------------------
+module Web.Authenticate.Rpxnow
+    ( Identifier (..)
+    , authenticate
+    ) where
+
+import Text.JSON
+import Network.HTTP.Wget
+import Data.Maybe (isJust, fromJust)
+
+-- | Information received from Rpxnow after a valid login.
+data Identifier = Identifier
+    { identifier :: String
+    , extraData :: [(String, String)]
+    }
+
+-- | Attempt to log a user in.
+authenticate :: Monad m
+             => String -- ^ API key given by RPXNOW.
+             -> String -- ^ Token passed by client.
+             -> IO (m Identifier)
+authenticate apiKey token = do
+    body <- wget
+                "https://rpxnow.com/api/v2/auth_info"
+                []
+                [ ("apiKey", apiKey)
+                , ("token", token)
+                ]
+    case body of
+        Left s -> return $ fail $ "Unable to connect to rpxnow: " ++ s
+        Right b ->
+          case decode b >>= getObject of
+            Error s -> return $ fail $ "Not a valid JSON response: " ++ s
+            Ok o ->
+              case valFromObj "stat" o of
+                Error _ -> return $ fail "Missing 'stat' field"
+                Ok "ok" -> return $ parseProfile o
+                Ok stat -> return $ fail $ "Login not accepted: " ++ stat
+
+parseProfile :: Monad m => JSObject JSValue -> m Identifier
+parseProfile v = do
+    profile <- resultToMonad $ valFromObj "profile" v >>= getObject
+    ident <- resultToMonad $ valFromObj "identifier" profile
+    let pairs = fromJSObject profile
+        pairs' = filter (\(k, _) -> k /= "identifier") pairs
+        pairs'' = map fromJust . filter isJust . map takeString $ pairs'
+    return $ Identifier ident pairs''
+
+takeString :: (String, JSValue) -> Maybe (String, String)
+takeString (k, JSString v) = Just (k, fromJSString v)
+takeString _ = Nothing
+
+getObject :: Monad m => JSValue -> m (JSObject JSValue)
+getObject (JSObject o) = return o
+getObject _ = fail "Not an object"
+
+resultToMonad :: Monad m => Result a -> m a
+resultToMonad (Ok x) = return x
+resultToMonad (Error s) = fail s
diff --git a/authenticate.cabal b/authenticate.cabal
new file mode 100644
--- /dev/null
+++ b/authenticate.cabal
@@ -0,0 +1,20 @@
+name:            authenticate
+version:         0.0.0
+license:         BSD3
+license-file:    LICENSE
+author:          Michael Snoyman <michael@snoyman.com>
+maintainer:      Michael Snoyman <michael@snoyman.com>
+synopsis:        Authentication methods for Haskell web applications.
+description:     Focus is on remote authentication methods, such as OpenID,
+                 rpxnow and Google.
+category:        Web
+stability:       unstable
+cabal-version:   >= 1.2
+build-type:      Simple
+homepage:        http://github.com/snoyberg/authenticate/tree/master
+
+library
+    build-depends:   base, json, http-wget, tagsoup
+    exposed-modules: Web.Authenticate.Rpxnow,
+                     Web.Authenticate.OpenId
+    ghc-options:     -Wall -fno-warn-orphans
