summaryrefslogtreecommitdiff
path: root/Biz/Auth.hs
blob: 1c3e45ccff7f6821e43de435cb9c6ea0526a4636 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}

-- | A module for common OAuth flows.
--
-- Consider using authenticate-oauth package
module Biz.Auth
  ( -- * OAuth
    OAuthResponse (..),
    OAuthArgs (..),

    -- * GitHub Authentication
    GitHub (..),
    githubOauth,

    -- * Servant Helpers
    SetCookies,
  )
where

import Alpha
import qualified Data.Aeson as Aeson
import Network.HTTP.Req ((/:), (=:))
import qualified Network.HTTP.Req as Req
import Servant (Header, Headers)
import qualified Servant.Auth.Server as Auth
import qualified System.Envy as Envy

-- | Use this instead of 'mempty' for explicity.
notset :: Text
notset = "notset"

-- | Wrapper around 'Auth.SetCookie' that you can put in a servant path
-- descriptor.
type SetCookies ret =
  (Headers '[Header "Set-Cookie" Auth.SetCookie, Header "Set-Cookie" Auth.SetCookie] ret)

-- | These are arguments that a 3rd-party OAuth provider needs in order for us
-- to authenticate a user.
data OAuthArgs = OAuthArgs
  { clientSecret :: Text,
    clientId :: Text,
    clientState :: Text
  }
  deriving (Generic, Show)

instance Envy.DefConfig OAuthArgs where
  defConfig =
    OAuthArgs
      { clientSecret = notset,
        clientId = notset,
        clientState = notset
      }

instance Envy.FromEnv OAuthArgs

-- | A type for parsing JSON auth responses, used in 'githubOauth' below.
-- Should be moved to Biz.Auth with others.
data OAuthResponse = OAuthResponse
  { access_token :: Text,
    scope :: Text,
    token_type :: Text
  }
  deriving (Generic, Aeson.FromJSON)

newtype GitHub = GitHub OAuthArgs
  deriving (Show, Generic)

instance Envy.DefConfig GitHub where
  defConfig =
    GitHub
      <| OAuthArgs
        { clientSecret = notset,
          clientId = notset,
          clientState = notset
        }

instance Envy.FromEnv GitHub where
  fromEnv Nothing =
    GitHub
      </ Envy.gFromEnvCustom
        Envy.Option
          { Envy.dropPrefixCount = 0,
            Envy.customPrefix = "GITHUB"
          }
        Nothing
  fromEnv (Just (GitHub x)) =
    GitHub
      </ Envy.gFromEnvCustom
        Envy.Option
          { Envy.dropPrefixCount = 0,
            Envy.customPrefix = "GITHUB"
          }
        (Just x)

-- | POST to GitHub's OAuth service and get the user's oAuth token.
githubOauth ::
  GitHub ->
  -- | This should be GitHub.Token but GitHub.Auth doesn't export Token.
  Text ->
  IO OAuthResponse
githubOauth (GitHub OAuthArgs {..}) code =
  accessTokenRequest
    /> Req.responseBody
    |> Req.runReq Req.defaultHttpConfig
  where
    accessTokenRequest :: Req.Req (Req.JsonResponse OAuthResponse)
    accessTokenRequest =
      Req.req
        Req.POST
        (Req.https "github.com" /: "login" /: "oauth" /: "access_token")
        Req.NoReqBody
        Req.jsonResponse
        <| "client_id" =: clientId
        <> "client_secret" =: clientSecret
        <> "code" =: code
        <> "state" =: clientState