summaryrefslogtreecommitdiff
path: root/fig-web/src/Fig/Web/Module/TwitchAuth.hs
blob: 80d23805e7f922cad612db540d6ac981a68272c9 (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
module Fig.Web.Module.TwitchAuth
  ( public
  ) where

import Fig.Prelude

import qualified Network.HTTP.Req as R

import Data.Maybe (mapMaybe)
import qualified Data.Text as Text
import qualified Data.Text.Lazy as Text.L
import qualified Data.Map.Strict as Map
import qualified Data.Aeson as Aeson
import qualified Data.Aeson.Types as Aeson

import qualified Web.Scotty as Sc

import qualified Jose.Jwk as Jwk
import qualified Jose.Jwt as Jwt

import Fig.Web.Utils
import Fig.Web.Types

public :: PublicModule
public a = do
  onGet "/api/register" $ twitchAuthed a.cfg \auth -> do
    log "Authenticated with Twitch, trying to register..."
    let user = Text.toLower auth.name
    resetUserPassword a.cfg user auth.id >>= \case
      Nothing -> do
        log "Failed to register user"
        status status500
        respondText "failed to register"
      Just pass -> do
        log "Successfully registered user, responding..."
        respondText $ user <> " " <> pass
  onGet "/api/check" $ twitchAuthed a.cfg \auth -> do
    respondJSON @[Text] [auth.id, auth.name]

data TokenContents = TokenContents
  { aud :: !Text
  , exp :: !Int
  , iat :: !Int
  , iss :: !Text
  , sub :: !Text
  , azp :: !(Maybe Text)
  , nonce :: !Text
  , preferred_username :: !Text
  } deriving (Show, Eq, Generic)
instance Aeson.FromJSON TokenContents

fetchJwk :: MonadIO m => m (Maybe Jwk.Jwk)
fetchJwk = do
  resp <- R.responseBody <$> R.runReq R.defaultHttpConfig do
    R.req R.GET (R.https "id.twitch.tv" R./: "oauth2" R./: "keys") R.NoReqBody R.jsonResponse mempty
  let mkeys = Aeson.parseMaybe (Aeson..: "keys") resp
  let mjwk = mkeys >>= headMay
  log $ tshow mjwk
  pure mjwk

validateToken :: MonadIO m => ByteString -> m (Maybe TokenContents)
validateToken encodedToken = fetchJwk >>= \case
  Nothing -> pure Nothing
  Just jwk -> liftIO (Jwt.decode [jwk] Nothing encodedToken) >>= \case
    Left err -> do
      log $ "Failed to decode token: " <> tshow err
      pure Nothing
    Right jwt -> do
      let contents = case jwt of
            Jwt.Unsecured bs -> bs
            Jwt.Jws (_, bs) -> bs
            Jwt.Jwe (_, bs) -> bs
      log $ tshow contents
      pure $ Aeson.decodeStrict contents

data Auth = Auth { id :: !Text, name :: !Text } deriving Show
checkAuth :: Config -> Sc.ActionM (Maybe Auth)
checkAuth cfg =
  Sc.header "Authorization"
  >>= \case
    Just authstrLazy -> do
      let authstr = drop 1 $ Text.splitOn " " $ Text.L.toStrict authstrLazy
      let pairs = Map.fromList $ flip mapMaybe authstr \s ->
            case Text.splitOn "=" s of
              [k, v] -> Just (k, Text.takeWhile (/='"') $ Text.drop 1 v)
              _other -> Nothing
      case (Map.lookup "token" pairs, Map.lookup "nonce" pairs) of
        (Just token, Just nonce) -> do
          validateToken (encodeUtf8 token) >>= \case
            Just tc
              | tc.aud == cfg.clientId
              , tc.nonce == nonce
                -> do
                  pure . Just $ Auth { name = tc.preferred_username
                    , id = tc.sub
                    }
            _else -> do
              pure Nothing
        _else -> pure Nothing
    _else -> pure Nothing

twitchAuthed :: Config -> (Auth -> Sc.ActionM ()) -> Sc.ActionM ()
twitchAuthed cfg f = checkAuth cfg >>= \case
  Nothing -> do
    status status401
    respondText "unauthorized"
  Just auth -> f auth