Как хранить сертификаты OAuth2 в Haskell - PullRequest
3 голосов
/ 11 октября 2019

Как правильно хранить OAuth2 jwk в haskell? Сертификаты, которые я получаю, получены от https://www.googleapis.com/oauth2/v3/certs, и я хотел бы не вызывать сертификаты каждый раз, когда мне нужно проверить подпись на токене. Варианты, кажется, MVar, TVar, IORef или монадой состояния, хотя я не совсем уверен, как бы я реализовал монаду состояния для этого.

Основные шаги будут следующими (запуск за скотти-сервером):

  1. Получить токен от IDP
  2. Декодировать Jwt с помощью JWk
  3. Если при декодировании происходит сбой из-за неверной подписи, проверьте конечную точку на наличие новых сертификатов и изменитетекущая переменная, содержащая сертификат

Я сейчас использую jose-jwt, wreq и scotty и у меня есть кое-что, что работает, но я хотел бы реализовать подход, о котором я спрашиваю, а не мой существующий подход.

module Main where


import ClassyPrelude
import Web.Scotty as S
import Network.Wreq as W
import Control.Lens as CL
import qualified Data.Text.Lazy as TL
import qualified Network.URI.Encode as URI
import Network.Wai.Middleware.RequestLogger
import Jose.Jwe
import Jose.Jwa
import Jose.Jwk
import Jose.Jwt
import Jose.Jws
import Data.Aeson
import qualified Data.HashMap.Strict as HM 
import qualified Data.Text as T
import qualified Data.List as DL
import qualified Data.ByteString.Base64 as B64

main :: IO ()
main = scotty 8080 $ do
  middleware logStdoutDev
  redirectCallback
  oauthCallback
  oauthGen
  home

home :: ScottyM ()
home = do
  S.get "/:word" $ do
    beam <- S.param "word"
    html $ mconcat ["<h1>Scotty, ", beam, " me up!</h1>"]

redirectCallback :: ScottyM ()
redirectCallback = do
  S.get "/redirect" $ do
    let v = uriSchemeBuilder
    redirect $ TL.fromStrict v

oauthCallback :: ScottyM ()
oauthCallback = do
  matchAny "/goauth2callback" $ do
    val <- body
    pars <- S.params
    c <- S.param "code" `rescue` (\_ -> return "haskell")
    let c1 = c <> (""::Text)
    r <- liftIO $ W.post "https://oauth2.googleapis.com/token" 
     [ "code" := (encodeUtf8 (c))
     , "client_id" := (encodeUtf8 consumerAccess)
     , "client_secret" := (encodeUtf8 consumerSecret)
     , "redirect_uri" := (encodeUtf8 redirectURI)
     , "grant_type" := ("authorization_code"::ByteString)
     , "access_type" := ("offline"::ByteString)
     ] 
    let newUser = (r ^? responseBody)
    case newUser of
     Just b -> do
      let jwt = decodeStrict (toStrict b) :: Maybe Value
      case jwt of
       Just (Object v) -> do
        let s = HM.lookup "id_token" v
        case s of
         Just (String k) -> do
          isValid <- liftIO $ validateToken (encodeUtf8 k)
          liftIO $ print isValid
          redirect "/hello_world" 
         _ -> redirect "/hello_world"  
       _ -> redirect "/hello_world"       
     Nothing -> redirect "/hello_world"


oauthGen :: ScottyM ()
oauthGen = do
  matchAny "/callback_gen" $ do
    val <- body
    redirect "/hello_world"

consumerAccess :: Text
consumerAccess = "google public key"

consumerSecret :: Text
consumerSecret = "google secret key"

oAuthScopes :: Text
oAuthScopes = "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email"

redirectURI :: Text
redirectURI = "http://localhost:8080/goauth2callback"

authURI :: Text
authURI = "https://accounts.google.com/o/oauth2/auth"

tokenURI :: Text
tokenURI = "https://oauth2.googleapis.com/token"

projectId :: Text
projectId = "project name"

responseType :: Text
responseType = "code"

oAuthUriBuilder :: [(Text, Text)]
oAuthUriBuilder = 
  [ ("client_id", consumerAccess)
  , ("redirect_uri", redirectURI)
  , ("scope", oAuthScopes)
  , ("response_type", responseType)
  ]

uriSchemeBuilder :: Text
uriSchemeBuilder = authURI <> "?" <> (foldr (\x y -> (fst x ++ "=" ++ (URI.encodeText $ snd x)) ++ "&" ++ y) "" oAuthUriBuilder)

validateToken :: ByteString -> IO (Either JwtError  JwtContent)
validateToken b = do
  keySet <- retrievePublicKeys
  case keySet of
   Left e -> return $ Left $ KeyError "No keyset supplied"
   Right k -> do
    let header = JwsEncoding RS256
    Jose.Jwt.decode k (Just $ header) b

retrievePublicKeys :: IO (Either String [Jwk])
retrievePublicKeys = do
 r <- liftIO $ W.get "https://www.googleapis.com/oauth2/v3/certs"
 case (r ^? responseBody) of
  Nothing -> return $ Left "No body in response from google oauth api"
  Just a -> do
   let v = eitherDecode a :: Either String Value
   case v of
    Left e -> return $ Left e
    Right (Object a) -> do
     let keySet = HM.lookup "keys" a
     case keySet of
      Just k -> do
       let kS = eitherDecode (Data.Aeson.encode k) :: Either String [Jwk]
       return $ kS
      _      -> return $ Left "No Key set provided"
    _ -> return $ Left $ "Incorrect response type from https://www.googleapis.com/oauth2/v3/certs"

Конкретная часть, которую я заинтересован в замене:

retrievePublicKeys :: IO (Either String [Jwk])
retrievePublicKeys = do
 r <- liftIO $ W.get "https://www.googleapis.com/oauth2/v3/certs"
 case (r ^? responseBody) of
  Nothing -> return $ Left "No body in response from google oauth api"
  Just a -> do
   let v = eitherDecode a :: Either String Value
   case v of
    Left e -> return $ Left e
    Right (Object a) -> do
     let keySet = HM.lookup "keys" a
     case keySet of
      Just k -> do
       let kS = eitherDecode (Data.Aeson.encode k) :: Either String [Jwk]
       return $ kS
      _      -> return $ Left "No Key set provided"
    _ -> return $ Left $ "Incorrect response type from https://www.googleapis.com/oauth2/v3/certs"

Я думал о хранении Jwk в redis, но я думаю, что есть лучший подход.

Ожидаемый результат - возможность безопасно изменять сертификат, который я получаю от Google, и использовать его при последующих расшифровках без необходимостипостоянно нажимайте на конечную точку.
(Примечание: да, я знаю, что это плохая практика - свернуть собственную безопасность, но это просто не в интересах)

1 Ответ

2 голосов
/ 11 октября 2019

Если вы выберете что-то вроде три слоя (ReaderT шаблон проектирования ), то кеширование IORef или TVar в средеучастие в ReaderT YourEnv IO было бы способом пойти. (atomicModifyIORef' должно быть достаточно.)

Ссылка Holmusk будет рекомендовать пакет jwt, но только что добавив, на другом языке на работе, вкэширование памяти сертификатов Google OAuth2, выбор библиотеки JWT в Haskell также очень похож на компромисс между функциями:

Например, jwt явно заявляет , что это не такпроверьте exp отметку времени истечения, но, насколько я вижу, jose-jwt даже не обращается к отметке времени истечения exp, которую он декодирует. google-oauth2-jwt делает и встраивает конечную точку (хорошо и плохо, труднее издеваться), но не предоставляет много эргономики, кроме этого. ( Редактировать: Похоже, что jose справляется с истечением , и что он также является самым популярным из тех, что я упоминал в Hackage.)

...