Implement confidential OIDC authentication boundary
This commit is contained in:
parent
0f888986d7
commit
a4e615096c
1 changed files with 1896 additions and 329 deletions
|
|
@ -1,27 +1,36 @@
|
|||
module Nuthatch.Config
|
||||
( Config (..)
|
||||
, AuthMode (..)
|
||||
, loadConfig
|
||||
) where
|
||||
|
||||
import Control.Monad (when)
|
||||
import qualified Data.ByteString as BS
|
||||
import Data.List (nub)
|
||||
import Data.Maybe (fromMaybe)
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.Text.Encoding as TE
|
||||
import System.Environment (lookupEnv)
|
||||
import Text.Read (readMaybe)
|
||||
|
||||
data AuthMode = AuthDev
|
||||
deriving stock (Eq, Show)
|
||||
|
||||
data Config = Config
|
||||
{ cfgListenHost :: String
|
||||
, cfgListenPort :: Int
|
||||
, cfgTlsCertPath :: FilePath
|
||||
, cfgTlsKeyPath :: FilePath
|
||||
, cfgPublicBaseUrl :: Text
|
||||
, cfgJwtSecret :: Text
|
||||
, cfgSessionHours :: Integer
|
||||
, cfgCookieName :: Text
|
||||
, cfgCsrfCookieName :: Text
|
||||
, cfgOidcTransactionCookieName :: Text
|
||||
, cfgOidcIssuer :: Text
|
||||
, cfgOidcClientId :: Text
|
||||
, cfgOidcAdditionalAudiences :: [Text]
|
||||
, cfgOidcClientSecret :: Text
|
||||
, cfgOidcRedirectUri :: Text
|
||||
, cfgOidcTransactionMinutes :: Integer
|
||||
, cfgOidcMaxIdTokenSeconds :: Integer
|
||||
, cfgPgConn :: String
|
||||
, cfgRedisHost :: String
|
||||
, cfgRedisPort :: Int
|
||||
|
|
@ -40,22 +49,45 @@ data Config = Config
|
|||
, cfgListCacheTtlSec :: Integer
|
||||
, cfgSearchMaxKeys :: Int
|
||||
, cfgRecoveryGraceSec :: Integer
|
||||
, cfgAuthMode :: AuthMode
|
||||
}
|
||||
deriving stock (Show)
|
||||
|
||||
loadConfig :: IO Config
|
||||
loadConfig = do
|
||||
listen <- fromMaybe "127.0.0.1:8080" <$> lookupEnv "NUTHATCH_LISTEN"
|
||||
let (host, portText) = splitHostPort listen
|
||||
port <- requireRead "NUTHATCH_LISTEN port" portText
|
||||
publicBase <- T.pack . fromMaybe ("http://" <> listen) <$> lookupEnv "NUTHATCH_PUBLIC_BASE_URL"
|
||||
tlsCertPath <- fromMaybe "/tls/tls.crt" <$> lookupEnv "NUTHATCH_TLS_CERT_PATH"
|
||||
tlsKeyPath <- fromMaybe "/tls/tls.key" <$> lookupEnv "NUTHATCH_TLS_KEY_PATH"
|
||||
publicBase <- T.pack <$> requireEnv "NUTHATCH_PUBLIC_BASE_URL"
|
||||
when (not ("https://" `T.isPrefixOf` publicBase)) $
|
||||
fail "NUTHATCH_PUBLIC_BASE_URL must use https"
|
||||
jwtSecret <- T.pack <$> requireEnv "NUTHATCH_JWT_SECRET"
|
||||
when (T.length jwtSecret < 16) $
|
||||
fail "NUTHATCH_JWT_SECRET must be at least 16 characters"
|
||||
when (BS.length (TE.encodeUtf8 jwtSecret) < 32) $
|
||||
fail "NUTHATCH_JWT_SECRET must be at least 32 bytes"
|
||||
sessionHours <- envInteger "NUTHATCH_SESSION_HOURS" 12
|
||||
cookieName <- T.pack . fromMaybe "nuthatch_session" <$> lookupEnv "NUTHATCH_COOKIE_NAME"
|
||||
csrfCookie <- T.pack . fromMaybe "nuthatch_csrf" <$> lookupEnv "NUTHATCH_CSRF_COOKIE_NAME"
|
||||
transactionCookie <- T.pack . fromMaybe "nuthatch_oidc" <$> lookupEnv "NUTHATCH_OIDC_TRANSACTION_COOKIE_NAME"
|
||||
oidcIssuer <- T.dropWhileEnd (== '/') . T.pack <$> requireEnv "NUTHATCH_OIDC_ISSUER"
|
||||
oidcClientId <- T.pack <$> requireEnv "NUTHATCH_OIDC_CLIENT_ID"
|
||||
additionalAudienceValue <- lookupEnv "NUTHATCH_OIDC_ADDITIONAL_AUDIENCES"
|
||||
let oidcAdditionalAudiences =
|
||||
case T.strip . T.pack <$> additionalAudienceValue of
|
||||
Nothing -> []
|
||||
Just "" -> []
|
||||
Just value -> map T.strip (T.splitOn "," value)
|
||||
when
|
||||
( any T.null oidcAdditionalAudiences
|
||||
|| oidcClientId `elem` oidcAdditionalAudiences
|
||||
|| length oidcAdditionalAudiences /= length (nub oidcAdditionalAudiences)
|
||||
) $
|
||||
fail "NUTHATCH_OIDC_ADDITIONAL_AUDIENCES must contain unique non-client values"
|
||||
oidcClientSecret <- T.pack <$> requireEnv "NUTHATCH_OIDC_CLIENT_SECRET"
|
||||
oidcRedirectUri <- T.pack <$> requireEnv "NUTHATCH_OIDC_REDIRECT_URI"
|
||||
when (oidcRedirectUri /= publicBase <> "/oidc/callback") $
|
||||
fail "NUTHATCH_OIDC_REDIRECT_URI must equal the public callback URL"
|
||||
oidcTransactionMinutes <- envInteger "NUTHATCH_OIDC_TRANSACTION_MINUTES" 5
|
||||
oidcMaxIdTokenSeconds <- envInteger "NUTHATCH_OIDC_MAX_ID_TOKEN_SECONDS" 3600
|
||||
pgConn <- requireEnv "NUTHATCH_PG_CONN"
|
||||
redisUrl <- requireEnv "NUTHATCH_REDIS_URL"
|
||||
(redisHost, redisPort, redisDb) <- parseRedisUrl redisUrl
|
||||
|
|
@ -73,18 +105,24 @@ loadConfig = do
|
|||
listTtl <- envInteger "NUTHATCH_LIST_CACHE_TTL_SEC" 30
|
||||
searchMax <- fromInteger <$> envInteger "NUTHATCH_SEARCH_MAX_KEYS" 500
|
||||
recoveryGrace <- envInteger "NUTHATCH_RECOVERY_GRACE_SEC" 90
|
||||
authModeText <- fromMaybe "dev" <$> lookupEnv "NUTHATCH_AUTH_MODE"
|
||||
authMode <- case authModeText of
|
||||
"dev" -> pure AuthDev
|
||||
other -> fail $ "unsupported NUTHATCH_AUTH_MODE: " <> other
|
||||
pure Config
|
||||
{ cfgListenHost = host
|
||||
, cfgListenPort = port
|
||||
, cfgTlsCertPath = tlsCertPath
|
||||
, cfgTlsKeyPath = tlsKeyPath
|
||||
, cfgPublicBaseUrl = publicBase
|
||||
, cfgJwtSecret = jwtSecret
|
||||
, cfgSessionHours = sessionHours
|
||||
, cfgCookieName = cookieName
|
||||
, cfgCsrfCookieName = csrfCookie
|
||||
, cfgOidcTransactionCookieName = transactionCookie
|
||||
, cfgOidcIssuer = oidcIssuer
|
||||
, cfgOidcClientId = oidcClientId
|
||||
, cfgOidcAdditionalAudiences = oidcAdditionalAudiences
|
||||
, cfgOidcClientSecret = oidcClientSecret
|
||||
, cfgOidcRedirectUri = oidcRedirectUri
|
||||
, cfgOidcTransactionMinutes = oidcTransactionMinutes
|
||||
, cfgOidcMaxIdTokenSeconds = oidcMaxIdTokenSeconds
|
||||
, cfgPgConn = pgConn
|
||||
, cfgRedisHost = redisHost
|
||||
, cfgRedisPort = redisPort
|
||||
|
|
@ -103,7 +141,6 @@ loadConfig = do
|
|||
, cfgListCacheTtlSec = listTtl
|
||||
, cfgSearchMaxKeys = searchMax
|
||||
, cfgRecoveryGraceSec = recoveryGrace
|
||||
, cfgAuthMode = authMode
|
||||
}
|
||||
|
||||
splitHostPort :: String -> (String, String)
|
||||
|
|
|
|||
Loading…
Reference in a new issue