diff options
| author | LLLL Colonq <llll@colonq> | 2025-05-26 04:43:38 -0400 |
|---|---|---|
| committer | LLLL Colonq <llll@colonq> | 2025-05-26 04:45:07 -0400 |
| commit | 1f2e453d0c9f8412b9032cb4e655713ecdcf1fa3 (patch) | |
| tree | c2e19550aeec4c092dceefb37a85497a4b90b485 /fig-web/src/Fig/Web/Module/TwitchAuth.hs | |
| parent | b5003a97d3f02b7c8cb5e63468b781d8d849264d (diff) | |
web: Refactor major style
Diffstat (limited to 'fig-web/src/Fig/Web/Module/TwitchAuth.hs')
| -rw-r--r-- | fig-web/src/Fig/Web/Module/TwitchAuth.hs | 107 |
1 files changed, 107 insertions, 0 deletions
diff --git a/fig-web/src/Fig/Web/Module/TwitchAuth.hs b/fig-web/src/Fig/Web/Module/TwitchAuth.hs new file mode 100644 index 0000000..4847da6 --- /dev/null +++ b/fig-web/src/Fig/Web/Module/TwitchAuth.hs @@ -0,0 +1,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 :: Module +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 |
