{"action":"created","comment":{"url":"https://api.github.com/repos/stackbuilders/tutorials/pulls/comments/74339633","id":74339633,"diff_hunk":"@@ -0,0 +1,566 @@\n+---\n+title: Servant authentication and sessions via cookies\n+published: 2016-08-05\n+ghc: 7.10.3\n+lts: 6.10\n+libraries: servant-auth-cookie-0.3.0.2 servant-0.8\n+language: haskell\n+author: Mark Karpov\n+author-name: Mark Karpov\n+description: Authentication is sort-of weak place in the Servant web framework. In this tutorial we build RESTful authentication in Servant storing all the session in an encrypted cookie client-side.\n+---\n+\n+Authentication in Servant is perhaps not as easy and powerful as it should\n+be. However, with Servant 0.5 and later it's possible to use the feature\n+called “generalized authentication” to add authentication that is closer to\n+real-world expectations than anything before. The feature is called\n+“experimental” in the Servant docs with the hope to get feedback from users\n+to see how good the idea is. In this tutorial we will provide that feedback\n+going through building of a small application that uses Servant for serving\n+user-facing content, not an API. We will be using the\n+[`servant-auth-cookie`](https://hackage.haskell.org/package/servant-auth-cookie)\n+library which we found and contributed to while adding authentication for\n+one of our projects at Stack Builders.\n+\n+To follow the tutorial, familiarity with the\n+[Servant web-framework](https://haskell-servant.github.io/) is expected.\n+\n+## Setting the goal\n+\n+Currently\n+[the Servant docs](http://haskell-servant.readthedocs.io/en/stable/tutorial/Authentication.html)\n+describe two ways to perform authentication with the framework: using\n+[basic authentication](https://en.wikipedia.org/wiki/Basic_access_authentication)\n+and with the above-mentioned generalized authentication. The both methods\n+are similar in how you use them in your code, yet generalized authentication\n+opens the possibility to read the cookie you might have previously set,\n+because it has access to request data.\n+\n+In this tutorial we will set and then use for authentication an encrypted\n+cookie with (possibly) arbitrary session data in it. All the session data\n+will be stored client-side. It seems to make sense to check cookie and\n+return a `Maybe Session` value to give the handlers more freedom in how to\n+use session data or the fact of its absence (for example, we will change the\n+application menu depending on whether the user is logged in or not — a\n+reasonable thing to expect from a web application).\n+\n+## Imports and language extensions\n+\n+Let's start by importing some modules and enabling a few language\n+extensions. The complete source code of this application is available in the\n+[Stack Builders's tutorials repository on GitHub](https://github.com/stackbuilders/tutorials),\n+so you can just clone the repository and start playing with it.\n+\n+```haskell\n+{-# LANGUAGE DataKinds            #-}\n+{-# LANGUAGE FlexibleInstances    #-}\n+{-# LANGUAGE OverloadedStrings    #-}\n+{-# LANGUAGE RecordWildCards      #-}\n+{-# LANGUAGE ScopedTypeVariables  #-}\n+{-# LANGUAGE TypeFamilies         #-}\n+{-# LANGUAGE TypeOperators        #-}\n+{-# OPTIONS_GHC -fno-warn-orphans #-}\n+\n+module Main (main) where\n+\n+import Control.Monad.Catch (try)\n+import Control.Monad.Except\n+import Control.Monad.Reader\n+import Crypto.Cipher.AES (AES256)\n+import Crypto.Cipher.Types (ctrCombine)\n+import Crypto.Hash.Algorithms (SHA256)\n+import Crypto.Random\n+import Data.ByteString (ByteString)\n+import Data.Serialize hiding (Get)\n+import Data.Text (Text)\n+import Lucid\n+import Network.URI hiding (scheme)\n+import Network.Wai\n+import Network.Wai.Handler.Warp\n+import Servant\n+import Servant.HTML.Lucid\n+import Servant.Server.Experimental.Auth\n+import Servant.Server.Experimental.Auth.Cookie\n+import qualified Data.ByteString    as B\n+import qualified Data.Text          as T\n+import qualified Data.Text.Encoding as T\n+```\n+\n+## The handler monad\n+\n+To work with cookies, we will need to be able to access at least three\n+values from handlers: `AuthCookieSettings` (more on that below),\n+`RandomSource`, and `ServerKey`. For type-safe rendering we will need three\n+more things: approot (as a `String`), port (`Int`), and scheme (`String`).\n+Knowing these requirements it's an obvious choice to use monad stack with\n+`ReaderT` parametrized over `AppContext` record containing all necessary\n+data that should be available in handlers:\n+\n+```haskell\n+-- | The handler monad, to work with cookies we need to have access to\n+-- 'AuthCookieSettings', 'RandomSource', and 'ServerKey'. It's also a handy\n+-- place to put some values that will be useful for type-safe route\n+-- rendering.\n+\n+type App = ReaderT AppContext (ExceptT ServantErr IO)\n+\n+-- | The application's context.\n+\n+data AppContext = AppContext\n+  { appContextAuthSettings :: AuthCookieSettings\n+  , appContextRandomSource :: RandomSource\n+  , appContextServerKey    :: ServerKey\n+  , appContextApproot      :: String\n+  , appContextPort         :: Int\n+  , appContextScheme       :: String\n+  }\n+```\n+\n+This should be pretty self-explanatory, so let's move on to the session data.\n+\n+## Session data\n+\n+First of all it's necessary to decide which data to include in session. It\n+will be stored in encrypted form, on client's machine in a cookie, which\n+means that every time client makes a request to your domain, all the data\n+will be sent to you — this is why we should not store anything big there. In\n+most cases an identifier of the logged in user and some basic information\n+that forms context of current session is stored. In our example, we will\n+store user name and email:\n+\n+```haskell\n+-- | A 'Session' in our case will include the user name and his\\/her email.\n+-- In a more realistic application this could as well include the user's id.\n+--\n+-- In order to store the 'Session' in a cookie, we need to make it an\n+-- instance of the 'Serialize' type class.\n+\n+data Session = Session\n+  { sessionUsername :: Text\n+  , sessionEmail    :: Text }\n+```\n+\n+To encrypt and send the information we need to know how to convert it into a\n+`ByteString` — binary representation of the data. The `servant-auth-cookie`\n+package uses the [`cereal`](https://hackage.haskell.org/package/cereal)\n+library for serialization, so we need to make `Session` an instance of\n+`Serialize` type class:\n+\n+```haskell\n+instance Serialize Session where\n+  put (Session username email) = do\n+    let putText txt =\n+          let bytes = T.encodeUtf8 txt\n+          in (putWord32le . fromIntegral . B.length) bytes\n+             >> putByteString bytes\n+    putText username\n+    putText email\n+  get = do\n+    let getText =\n+          getWord32le >>= fmap T.decodeUtf8 . getBytes . fromIntegral\n+    username <- getText\n+    email    <- getText\n+    return (Session username email)\n+```\n+\n+## Auth check and some boilerplate\n+\n+The next thing we are going to need is to specify the type of data we will\n+get as a result of `cookieAuthCheck`. The `servant-auth-cookie` package\n+defines the `AuthCookieData` open type family for that, let's define a type\n+instance:\n+\n+```haskell\n+type instance AuthCookieData = Maybe Session\n+```\n+\n+Now we are ready to write the check whose type is `AuthHandler Request\n+(Maybe Session)`:\n+\n+```haskell\n+-- | The acutal session check. The ‘servant-auth-cookie’ package provides\n+-- the 'defaultAuthHandler', but that does not indicate missing 'Session' as\n+-- 'Nothing', so we use the custom one.\n+\n+cookieAuthCheck\n+  :: AuthCookieSettings\n+  -> ServerKey\n+  -> AuthHandler Request (Maybe Session)\n+cookieAuthCheck authSettings serverKey = mkAuthHandler $ \\request -> do\n+  result <- try (getSession authSettings serverKey request)\n+  case result :: Either AuthCookieException (Maybe Session) of\n+    Left _ -> return Nothing\n+    Right session -> return session\n+```\n+\n+Now we can perform checks whether cookie is present and can be de-serialized\n+or not. Then goes some boilperplate:\n+\n+```haskell\n+\n+-- | The entry point of the application.\n+\n+main :: IO ()\n+main = do\n+  let appPort = 8000\n+  randomSource <- mkRandomSource drgNew 2000\n+  serverKey    <- mkServerKey 16 (Just $ fromIntegral (86400 :: Integer))\n+  let authSettings = AuthCookieSettings\n+        { acsSessionField = \"Session\"\n+        , acsCookieFlags  = [\"HttpOnly\"]\n+        , acsMaxAge       = fromIntegral (6 * 3600 :: Integer)\n+        , acsExpirationFormat = \"%0Y%m%d%H%M%S\"\n+        , acsPath         = \"/\"\n+        , acsHashAlgorithm = Proxy :: Proxy SHA256\n+        , acsCipher       = Proxy :: Proxy AES256\n+        , acsEncryptAlgorithm = ctrCombine\n+        , acsDecryptAlgorithm = ctrCombine }\n+  run appPort . app $ AppContext\n+    { appContextAuthSettings = authSettings\n+    , appContextRandomSource = randomSource\n+    , appContextServerKey    = serverKey\n+    , appContextApproot      = \"localhost\"\n+    , appContextPort         = appPort\n+    , appContextScheme       = \"http:\" }\n+\n+app :: AppContext -> Application\n+app appContext@AppContext {..} = serveWithContext\n+  (Proxy :: Proxy Routes)\n+  ((cookieAuthCheck appContextAuthSettings appContextServerKey\n+     :: AuthHandler Request (Maybe Session))\n+   :. EmptyContext)\n+  (server appContext)\n+\n+server :: AppContext -> Server Routes\n+server context = enter (Nat $ flip runReaderT context) handlers\n+```\n+\n+Note that we create `RandomSource` which is a source of random numbers that\n+will reset itself after producing 2000 random bytes. The source can be used\n+in concurrent code because the actual modification of its internal state is\n+atomic. `ServerKey` is another thing we are going to need for cookie\n+encryption, it has the ability to “expire” after specified period of time\n+(one day in our case, but if we passed `Nothing` it would never expire),\n+it's suitable for concurrent usage as well.\n+\n+Other things to note is the use of `serveWithContext` that is\n+well-documented in the Servant documentation\n+[here](https://www.stackage.org/haddock/nightly-2016-08-06/servant-server-0.7.1/Servant-Server.html#v:serveWithContext)\n+and the `enter` function that given natural transformation from `App` monad\n+to Servant's default `Handler` monad allows use to use our custom monad in\n+handlers.\n+\n+## Defining the routes\n+\n+First, let's define a type synonym for type that will represent auth check\n+in our routes:\n+\n+```haskell\n+-- | It's generally a good idea to have a type synonym for your\n+-- authentication type so it's easier to modify it later.\n+\n+type AppAuth = AuthProtect \"cookie-auth\"\n+\n+```\n+\n+We are going to render the routes. For that to work nicely with `AppAuth`,\n+we need to define the following orphan instance:\n+\n+```haskell\n+-- This orphan instance is necessary in order to teach Servant how to render\n+-- routes that use our authentication method.\n+\n+instance HasLink sub => HasLink (AppAuth :> sub) where\n+  type MkLink (AppAuth :> sub) = MkLink sub\n+  toLink _ = toLink (Proxy :: Proxy sub)\n+\n+```\n+\n+The rest is the definitions of all the endpoints our application is going to\n+have.\n+\n+```haskell\n+type Routes = GetHomeR\n+  :<|> GetSignInR\n+  :<|> GetMyPageR\n+  :<|> GetSignOutR\n+\n+type GetHomeR =\n+  AppAuth :>\n+  Get '[HTML] (AppView HomeView)\n+\n+type GetSignInR =\n+  \"sign-in\" :>\n+  Get '[HTML] (Headers '[Header \"set-cookie\" ByteString] (AppView SignInView))\n+\n+type GetMyPageR =\n+  AppAuth :>\n+  \"my-page\" :>\n+  Get '[HTML] (AppView MyPageView)\n+\n+type GetSignOutR =\n+  AppAuth :>\n+  \"sign-out\" :>\n+  Get '[HTML] (Headers '[Header \"set-cookie\" ByteString] (AppView SignOutView))\n+```\n+\n+Each of them is protected with `AppAuth` even though only the last two\n+really require protection. The `GetHomeR` route is protected to be able to\n+change the main menu for logged in users. The `GetSignInR` does not require\n+authentication in our application because when the page in rendered the user\n+is always logged in and we know how to render the page.\n+\n+## Defining views and their HTML representation\n+\n+Every route will return a value of distinct type that should be convertable\n+to HTML, since we use [`lucid`](https://hackage.haskell.org) for building\n+HTML data, we will need to make the views instances of the `ToHtml` type\n+class:\n+\n+```haskell\n+-- | Type of the wrapper view (sort of “default template”). This one needs\n+-- to have rendered links in order to interpolate them into the HTML data it\n+-- generates.\n+\n+data AppView view = AppView\n+  { appViewContent     :: view\n+  , appViewSession     :: Maybe Session\n+  , appViewHomeLink    :: Text\n+  , appViewSignInLink  :: Text\n+  , appViewMyPageLink  :: Text\n+  , appViewSignOutLink :: Text\n+  }\n+\n+instance ToHtml view => ToHtml (AppView view) where\n+  toHtml AppView {..} = doctypehtml_ $ do\n+    head_ $ do\n+      meta_ [charset_ \"utf-8\"]\n+      meta_ [httpEquiv_ \"x-ua-compatible\", content_ \"IE=edge\"]\n+      meta_ [name_ \"viewport\", content_ \"width=device-width,initial-scale=1\"]\n+    body_ $ do\n+      h1_ \"Servant Auth Cookie Demo\"\n+      hr_ []\n+      ul_ $ do\n+        li_ (a_ [href_ appViewHomeLink] \"Home\")\n+        case appViewSession of\n+          Nothing ->\n+            li_ (a_ [href_ appViewSignInLink] \"Sign In\")\n+          Just Session {..} -> do\n+            li_ (a_ [href_ appViewMyPageLink] \"My Page\")\n+            li_ (a_ [href_ appViewSignOutLink] \"Sign Out\")\n+      hr_ []\n+      toHtml appViewContent\n+  toHtmlRaw = toHtml\n+\n+-- | Home page does not have anything special on it.\n+\n+data HomeView = HomeView\n+\n+instance ToHtml HomeView where\n+  toHtml HomeView = do\n+    h1_ \"Home Page\"\n+    \"Welcome Home!\"\n+  toHtmlRaw = toHtml\n+\n+-- | Sign in view can show a message.\n+\n+data SignInView = SignInView Text\n+\n+instance ToHtml SignInView where\n+  toHtml (SignInView msg) = do\n+    h1_ \"Sign In\"\n+    \"The app says:\"\n+    strong_ (toHtml msg)\n+  toHtmlRaw = toHtml\n+\n+data MyPageView = MyPageView Text Text\n+\n+instance ToHtml MyPageView where\n+  toHtml (MyPageView username email) = do\n+    h1_ \"My Page\"\n+    \"This is a page of user with the following data:\"\n+    ul_ $ do\n+      li_ (\"Username:\" >> strong_ (toHtml username))\n+      li_ (\"Email:\"    >> strong_ (toHtml email))\n+  toHtmlRaw = toHtml\n+\n+data SignOutView = SignOutView Text\n+\n+instance ToHtml SignOutView where\n+  toHtml (SignOutView msg) = do\n+    h1_ \"Sign Out\"\n+    \"The app says:\"\n+    strong_ (toHtml msg)\n+  toHtmlRaw = toHtml\n+```\n+\n+## Useful helpers for handler writing\n+\n+Before we go on to write the actual handlers, we are going to need a number\n+of helpers that seems to be very handy in this application and even more\n+handy in real ones.\n+\n+The first collection of functions deal with route rendering.\n+\n+```haskell\n+-- | Get textual representation for specific endpoint on the site.\n+\n+routeToText :: (IsElem a Routes, HasLink a, MkLink a ~ URI)\n+  => Proxy a           -- ^ The 'Proxy' clarifying type of route to render\n+  -> App Text      -- ^ The rendered route as 'Text'\n+routeToText = renderURI . routeToURI\n+\n+-- | Get link representation for specific endpoint on the site.\n+\n+routeToURI :: (IsElem a Routes, HasLink a) => Proxy a -> MkLink a\n+routeToURI = safeLink (Proxy :: Proxy Routes)\n+\n+-- | Render an 'URI' as 'Text'.\n+\n+renderURI :: URI -> App Text\n+renderURI uri = do\n+  approot <- asks appContextApproot\n+  port    <- asks appContextPort\n+  scheme  <- asks appContextScheme\n+  let uri' = uri\n+        { uriScheme = scheme\n+        , uriAuthority = Just URIAuth\n+          { uriUserInfo = \"\"\n+          , uriRegName = approot\n+          , uriPort = ':' : show port }\n+        , uriPath = '/' : uriPath uri }\n+  (return . T.pack) (uriToString (const \"\") uri' \"\")\n+```\n+\n+Here we are interested mainly in `routeToText`, because our routes do not\n+have captures or query parametrs and thus values produced by `safeLink`\n+(provided by Servant itself) return us `URI` values directly. The `safeLink`\n+function is pure, but to render a link to `Text` we need things like\n+approot, port number, and scheme which come from the `App` monad, hence the\n+type.\n+\n+The `mkAppView` function helps with populating links-related fields of\n+`AppView` record using the `routeToText` function we just defined:\n+\n+```haskell\n+-- | Create an 'AppView' this is the recommended method to created it, since\n+-- it initializes fields like 'appViewHomeLink' for you.\n+\n+mkAppView\n+  :: Maybe Session     -- ^ Active 'Session' if any\n+  -> view              -- ^ Actual view to insert into 'AppView'\n+  -> App (AppView view) -- ^ The resulting view\n+mkAppView appViewSession appViewContent = do\n+  appViewHomeLink    <- routeToText (Proxy :: Proxy GetHomeR)\n+  appViewSignInLink  <- routeToText (Proxy :: Proxy GetSignInR)\n+  appViewMyPageLink  <- routeToText (Proxy :: Proxy GetMyPageR)\n+  appViewSignOutLink <- routeToText (Proxy :: Proxy GetSignOutR)\n+  return AppView {..}\n+\n+```\n+\n+Lastly, since we get `Nothing` when the cookie is not set or cannot be\n+deserialized, even non-authorized calls get to handlers. To ensure that only\n+logged-in users can receive responses from some handlers the following\n+simple helper can be used:\n+\n+```haskell\n+-- | Perform actions with 'Session' or return 403 HTTP status code.\n+\n+withSession\n+  :: Maybe Session     -- ^ 'Session', if any\n+  -> (Session -> App a) -- ^ Callback making use of 'Session'\n+  -> App a\n+withSession ms action = maybe (throwError err403) action ms\n+```\n+\n+Now we are set for success in writing our handlers!\n+\n+## Defining handlers\n+\n+It's time to use all the goodies we have defined so far to put together our\n+handlers:\n+\n+```haskell\n+-- | The collection of all handlers.\n+\n+handlers :: ServerT Routes App\n+handlers = getHomeR\n+  :<|> getSignInR\n+  :<|> getMyPageR\n+  :<|> getSignOutR\n+\n+-- | The home page does not do anything fancy, although just like all the\n+-- other pages it features the menu that changes depending on whether the\n+-- user is logged in or not.\n+\n+getHomeR :: Maybe Session -> App (AppView HomeView)\n+getHomeR ms = mkAppView ms HomeView\n+\n+-- | The “Sign In” handler set the cookie and displayes a message.\n+\n+getSignInR :: App (Headers '[Header \"set-cookie\" ByteString] (AppView SignInView))\n+getSignInR = do\n+  let session = Session \"mark\" \"mark@example.org\"\n+  AppContext {..} <- ask\n+  mkAppView (Just session) (SignInView \"You have signed in.\")\n+    >>= addSession appContextAuthSettings appContextRandomSource appContextServerKey session\n+\n+-- | Due to the use of the 'withSession' helper, this page is only available\n+-- to logged in users.\n+\n+getMyPageR :: Maybe Session -> App (AppView MyPageView)\n+getMyPageR ms = withSession ms $ \\Session {..} ->\n+  mkAppView ms (MyPageView sessionUsername sessionEmail)\n+\n+-- | The “Sign Out” page sets cookies to empty byte string destroyng the data.\n+\n+getSignOutR\n+  :: Maybe Session\n+  -> App (Headers '[Header \"set-cookie\" ByteString] (AppView SignOutView))\n+getSignOutR ms = withSession ms $ \\_ -> do\n+  AppContext {..} <- ask\n+  mkAppView Nothing (SignOutView \"You have signed out.\")\n+    >>= addSession appContextAuthSettings appContextRandomSource appContextServerKey ()\n+```\n+\n+As you can see the only interesting moment here in the use of `addSession`","path":"tutorials/haskell/servant-auth/tutorial.md","position":527,"original_position":527,"commit_id":"2be6f3ad81e8a463b65151fc023a44feae9f8a16","original_commit_id":"2be6f3ad81e8a463b65151fc023a44feae9f8a16","user":{"login":"jpvillaisaza","id":584947,"avatar_url":"https://avatars.githubusercontent.com/u/584947?v=3","gravatar_id":"","url":"https://api.github.com/users/jpvillaisaza","html_url":"https://github.com/jpvillaisaza","followers_url":"https://api.github.com/users/jpvillaisaza/followers","following_url":"https://api.github.com/users/jpvillaisaza/following{/other_user}","gists_url":"https://api.github.com/users/jpvillaisaza/gists{/gist_id}","starred_url":"https://api.github.com/users/jpvillaisaza/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jpvillaisaza/subscriptions","organizations_url":"https://api.github.com/users/jpvillaisaza/orgs","repos_url":"https://api.github.com/users/jpvillaisaza/repos","events_url":"https://api.github.com/users/jpvillaisaza/events{/privacy}","received_events_url":"https://api.github.com/users/jpvillaisaza/received_events","type":"User","site_admin":false},"body":"@mrkkrp \"is the use\"?","created_at":"2016-08-10T22:02:48Z","updated_at":"2016-08-10T22:02:48Z","html_url":"https://github.com/stackbuilders/tutorials/pull/34#discussion_r74339633","pull_request_url":"https://api.github.com/repos/stackbuilders/tutorials/pulls/34","_links":{"self":{"href":"https://api.github.com/repos/stackbuilders/tutorials/pulls/comments/74339633"},"html":{"href":"https://github.com/stackbuilders/tutorials/pull/34#discussion_r74339633"},"pull_request":{"href":"https://api.github.com/repos/stackbuilders/tutorials/pulls/34"}}},"pull_request":{"url":"https://api.github.com/repos/stackbuilders/tutorials/pulls/34","id":80290454,"html_url":"https://github.com/stackbuilders/tutorials/pull/34","diff_url":"https://github.com/stackbuilders/tutorials/pull/34.diff","patch_url":"https://github.com/stackbuilders/tutorials/pull/34.patch","issue_url":"https://api.github.com/repos/stackbuilders/tutorials/issues/34","number":34,"state":"open","locked":false,"title":"Tutorial about cookie auth in Servant","user":{"login":"mrkkrp","id":8165792,"avatar_url":"https://avatars.githubusercontent.com/u/8165792?v=3","gravatar_id":"","url":"https://api.github.com/users/mrkkrp","html_url":"https://github.com/mrkkrp","followers_url":"https://api.github.com/users/mrkkrp/followers","following_url":"https://api.github.com/users/mrkkrp/following{/other_user}","gists_url":"https://api.github.com/users/mrkkrp/gists{/gist_id}","starred_url":"https://api.github.com/users/mrkkrp/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrkkrp/subscriptions","organizations_url":"https://api.github.com/users/mrkkrp/orgs","repos_url":"https://api.github.com/users/mrkkrp/repos","events_url":"https://api.github.com/users/mrkkrp/events{/privacy}","received_events_url":"https://api.github.com/users/mrkkrp/received_events","type":"User","site_admin":false},"body":"Status: the application is ready, first complete version of the text is ready as well.","created_at":"2016-08-05T18:56:17Z","updated_at":"2016-08-10T22:02:48Z","closed_at":null,"merged_at":null,"merge_commit_sha":"bac2d9cc74afcc02deddc5da824d39f0290b8d2f","assignee":null,"assignees":[],"milestone":null,"commits_url":"https://api.github.com/repos/stackbuilders/tutorials/pulls/34/commits","review_comments_url":"https://api.github.com/repos/stackbuilders/tutorials/pulls/34/comments","review_comment_url":"https://api.github.com/repos/stackbuilders/tutorials/pulls/comments{/number}","comments_url":"https://api.github.com/repos/stackbuilders/tutorials/issues/34/comments","statuses_url":"https://api.github.com/repos/stackbuilders/tutorials/statuses/2be6f3ad81e8a463b65151fc023a44feae9f8a16","head":{"label":"stackbuilders:servant-auth-tutorial","ref":"servant-auth-tutorial","sha":"2be6f3ad81e8a463b65151fc023a44feae9f8a16","user":{"login":"stackbuilders","id":526755,"avatar_url":"https://avatars.githubusercontent.com/u/526755?v=3","gravatar_id":"","url":"https://api.github.com/users/stackbuilders","html_url":"https://github.com/stackbuilders","followers_url":"https://api.github.com/users/stackbuilders/followers","following_url":"https://api.github.com/users/stackbuilders/following{/other_user}","gists_url":"https://api.github.com/users/stackbuilders/gists{/gist_id}","starred_url":"https://api.github.com/users/stackbuilders/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/stackbuilders/subscriptions","organizations_url":"https://api.github.com/users/stackbuilders/orgs","repos_url":"https://api.github.com/users/stackbuilders/repos","events_url":"https://api.github.com/users/stackbuilders/events{/privacy}","received_events_url":"https://api.github.com/users/stackbuilders/received_events","type":"Organization","site_admin":false},"repo":{"id":56546891,"name":"tutorials","full_name":"stackbuilders/tutorials","owner":{"login":"stackbuilders","id":526755,"avatar_url":"https://avatars.githubusercontent.com/u/526755?v=3","gravatar_id":"","url":"https://api.github.com/users/stackbuilders","html_url":"https://github.com/stackbuilders","followers_url":"https://api.github.com/users/stackbuilders/followers","following_url":"https://api.github.com/users/stackbuilders/following{/other_user}","gists_url":"https://api.github.com/users/stackbuilders/gists{/gist_id}","starred_url":"https://api.github.com/users/stackbuilders/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/stackbuilders/subscriptions","organizations_url":"https://api.github.com/users/stackbuilders/orgs","repos_url":"https://api.github.com/users/stackbuilders/repos","events_url":"https://api.github.com/users/stackbuilders/events{/privacy}","received_events_url":"https://api.github.com/users/stackbuilders/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/stackbuilders/tutorials","description":"Stack Builders Tutorials.","fork":false,"url":"https://api.github.com/repos/stackbuilders/tutorials","forks_url":"https://api.github.com/repos/stackbuilders/tutorials/forks","keys_url":"https://api.github.com/repos/stackbuilders/tutorials/keys{/key_id}","collaborators_url":"https://api.github.com/repos/stackbuilders/tutorials/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/stackbuilders/tutorials/teams","hooks_url":"https://api.github.com/repos/stackbuilders/tutorials/hooks","issue_events_url":"https://api.github.com/repos/stackbuilders/tutorials/issues/events{/number}","events_url":"https://api.github.com/repos/stackbuilders/tutorials/events","assignees_url":"https://api.github.com/repos/stackbuilders/tutorials/assignees{/user}","branches_url":"https://api.github.com/repos/stackbuilders/tutorials/branches{/branch}","tags_url":"https://api.github.com/repos/stackbuilders/tutorials/tags","blobs_url":"https://api.github.com/repos/stackbuilders/tutorials/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/stackbuilders/tutorials/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/stackbuilders/tutorials/git/refs{/sha}","trees_url":"https://api.github.com/repos/stackbuilders/tutorials/git/trees{/sha}","statuses_url":"https://api.github.com/repos/stackbuilders/tutorials/statuses/{sha}","languages_url":"https://api.github.com/repos/stackbuilders/tutorials/languages","stargazers_url":"https://api.github.com/repos/stackbuilders/tutorials/stargazers","contributors_url":"https://api.github.com/repos/stackbuilders/tutorials/contributors","subscribers_url":"https://api.github.com/repos/stackbuilders/tutorials/subscribers","subscription_url":"https://api.github.com/repos/stackbuilders/tutorials/subscription","commits_url":"https://api.github.com/repos/stackbuilders/tutorials/commits{/sha}","git_commits_url":"https://api.github.com/repos/stackbuilders/tutorials/git/commits{/sha}","comments_url":"https://api.github.com/repos/stackbuilders/tutorials/comments{/number}","issue_comment_url":"https://api.github.com/repos/stackbuilders/tutorials/issues/comments{/number}","contents_url":"https://api.github.com/repos/stackbuilders/tutorials/contents/{+path}","compare_url":"https://api.github.com/repos/stackbuilders/tutorials/compare/{base}...{head}","merges_url":"https://api.github.com/repos/stackbuilders/tutorials/merges","archive_url":"https://api.github.com/repos/stackbuilders/tutorials/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/stackbuilders/tutorials/downloads","issues_url":"https://api.github.com/repos/stackbuilders/tutorials/issues{/number}","pulls_url":"https://api.github.com/repos/stackbuilders/tutorials/pulls{/number}","milestones_url":"https://api.github.com/repos/stackbuilders/tutorials/milestones{/number}","notifications_url":"https://api.github.com/repos/stackbuilders/tutorials/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/stackbuilders/tutorials/labels{/name}","releases_url":"https://api.github.com/repos/stackbuilders/tutorials/releases{/id}","deployments_url":"https://api.github.com/repos/stackbuilders/tutorials/deployments","created_at":"2016-04-18T22:34:16Z","updated_at":"2016-06-29T13:52:02Z","pushed_at":"2016-08-10T19:41:39Z","git_url":"git://github.com/stackbuilders/tutorials.git","ssh_url":"git@github.com:stackbuilders/tutorials.git","clone_url":"https://github.com/stackbuilders/tutorials.git","svn_url":"https://github.com/stackbuilders/tutorials","homepage":"https://www.stackbuilders.com/tutorials/","size":1817,"stargazers_count":8,"watchers_count":8,"language":"Haskell","has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":4,"forks":0,"open_issues":4,"watchers":8,"default_branch":"tutorials"}},"base":{"label":"stackbuilders:tutorials","ref":"tutorials","sha":"a81a7f60ec1c379ec439ddfbe085b0ddf9acaf18","user":{"login":"stackbuilders","id":526755,"avatar_url":"https://avatars.githubusercontent.com/u/526755?v=3","gravatar_id":"","url":"https://api.github.com/users/stackbuilders","html_url":"https://github.com/stackbuilders","followers_url":"https://api.github.com/users/stackbuilders/followers","following_url":"https://api.github.com/users/stackbuilders/following{/other_user}","gists_url":"https://api.github.com/users/stackbuilders/gists{/gist_id}","starred_url":"https://api.github.com/users/stackbuilders/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/stackbuilders/subscriptions","organizations_url":"https://api.github.com/users/stackbuilders/orgs","repos_url":"https://api.github.com/users/stackbuilders/repos","events_url":"https://api.github.com/users/stackbuilders/events{/privacy}","received_events_url":"https://api.github.com/users/stackbuilders/received_events","type":"Organization","site_admin":false},"repo":{"id":56546891,"name":"tutorials","full_name":"stackbuilders/tutorials","owner":{"login":"stackbuilders","id":526755,"avatar_url":"https://avatars.githubusercontent.com/u/526755?v=3","gravatar_id":"","url":"https://api.github.com/users/stackbuilders","html_url":"https://github.com/stackbuilders","followers_url":"https://api.github.com/users/stackbuilders/followers","following_url":"https://api.github.com/users/stackbuilders/following{/other_user}","gists_url":"https://api.github.com/users/stackbuilders/gists{/gist_id}","starred_url":"https://api.github.com/users/stackbuilders/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/stackbuilders/subscriptions","organizations_url":"https://api.github.com/users/stackbuilders/orgs","repos_url":"https://api.github.com/users/stackbuilders/repos","events_url":"https://api.github.com/users/stackbuilders/events{/privacy}","received_events_url":"https://api.github.com/users/stackbuilders/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/stackbuilders/tutorials","description":"Stack Builders Tutorials.","fork":false,"url":"https://api.github.com/repos/stackbuilders/tutorials","forks_url":"https://api.github.com/repos/stackbuilders/tutorials/forks","keys_url":"https://api.github.com/repos/stackbuilders/tutorials/keys{/key_id}","collaborators_url":"https://api.github.com/repos/stackbuilders/tutorials/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/stackbuilders/tutorials/teams","hooks_url":"https://api.github.com/repos/stackbuilders/tutorials/hooks","issue_events_url":"https://api.github.com/repos/stackbuilders/tutorials/issues/events{/number}","events_url":"https://api.github.com/repos/stackbuilders/tutorials/events","assignees_url":"https://api.github.com/repos/stackbuilders/tutorials/assignees{/user}","branches_url":"https://api.github.com/repos/stackbuilders/tutorials/branches{/branch}","tags_url":"https://api.github.com/repos/stackbuilders/tutorials/tags","blobs_url":"https://api.github.com/repos/stackbuilders/tutorials/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/stackbuilders/tutorials/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/stackbuilders/tutorials/git/refs{/sha}","trees_url":"https://api.github.com/repos/stackbuilders/tutorials/git/trees{/sha}","statuses_url":"https://api.github.com/repos/stackbuilders/tutorials/statuses/{sha}","languages_url":"https://api.github.com/repos/stackbuilders/tutorials/languages","stargazers_url":"https://api.github.com/repos/stackbuilders/tutorials/stargazers","contributors_url":"https://api.github.com/repos/stackbuilders/tutorials/contributors","subscribers_url":"https://api.github.com/repos/stackbuilders/tutorials/subscribers","subscription_url":"https://api.github.com/repos/stackbuilders/tutorials/subscription","commits_url":"https://api.github.com/repos/stackbuilders/tutorials/commits{/sha}","git_commits_url":"https://api.github.com/repos/stackbuilders/tutorials/git/commits{/sha}","comments_url":"https://api.github.com/repos/stackbuilders/tutorials/comments{/number}","issue_comment_url":"https://api.github.com/repos/stackbuilders/tutorials/issues/comments{/number}","contents_url":"https://api.github.com/repos/stackbuilders/tutorials/contents/{+path}","compare_url":"https://api.github.com/repos/stackbuilders/tutorials/compare/{base}...{head}","merges_url":"https://api.github.com/repos/stackbuilders/tutorials/merges","archive_url":"https://api.github.com/repos/stackbuilders/tutorials/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/stackbuilders/tutorials/downloads","issues_url":"https://api.github.com/repos/stackbuilders/tutorials/issues{/number}","pulls_url":"https://api.github.com/repos/stackbuilders/tutorials/pulls{/number}","milestones_url":"https://api.github.com/repos/stackbuilders/tutorials/milestones{/number}","notifications_url":"https://api.github.com/repos/stackbuilders/tutorials/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/stackbuilders/tutorials/labels{/name}","releases_url":"https://api.github.com/repos/stackbuilders/tutorials/releases{/id}","deployments_url":"https://api.github.com/repos/stackbuilders/tutorials/deployments","created_at":"2016-04-18T22:34:16Z","updated_at":"2016-06-29T13:52:02Z","pushed_at":"2016-08-10T19:41:39Z","git_url":"git://github.com/stackbuilders/tutorials.git","ssh_url":"git@github.com:stackbuilders/tutorials.git","clone_url":"https://github.com/stackbuilders/tutorials.git","svn_url":"https://github.com/stackbuilders/tutorials","homepage":"https://www.stackbuilders.com/tutorials/","size":1817,"stargazers_count":8,"watchers_count":8,"language":"Haskell","has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":4,"forks":0,"open_issues":4,"watchers":8,"default_branch":"tutorials"}},"_links":{"self":{"href":"https://api.github.com/repos/stackbuilders/tutorials/pulls/34"},"html":{"href":"https://github.com/stackbuilders/tutorials/pull/34"},"issue":{"href":"https://api.github.com/repos/stackbuilders/tutorials/issues/34"},"comments":{"href":"https://api.github.com/repos/stackbuilders/tutorials/issues/34/comments"},"review_comments":{"href":"https://api.github.com/repos/stackbuilders/tutorials/pulls/34/comments"},"review_comment":{"href":"https://api.github.com/repos/stackbuilders/tutorials/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/stackbuilders/tutorials/pulls/34/commits"},"statuses":{"href":"https://api.github.com/repos/stackbuilders/tutorials/statuses/2be6f3ad81e8a463b65151fc023a44feae9f8a16"}}}}
{"actor":{"display_login":"jpvillaisaza"}}