Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ See `local.env` for example values which are used for local development.
- `SHARE_SERVER_PORT`: Which port the server should bind to on startup. This may differ from `SHARE_API_ORIGIN` if you're using a reverse proxy.
- `SHARE_REDIS`: The URL of the redis server.
- `SHARE_POSTGRES`: The URL of the postgres server.
- `SHARE_HMAC_KEY`: A secret key used for cryptographic signing. This should be at least 32 characters long.
- `SHARE_HMAC_KEY`: A secret key used for cryptographic signing of HashJWTs. This should be at least 32 characters long.
- `SHARE_EDDSA_KEY`: A secret key used for cryptographic signing of user sessions. This should be at least 32 characters long.
- `SHARE_DEPLOYMENT`: The deployment environment. One of: `local`, `staging`, `prod`.
- `SHARE_POSTGRES_CONN_TTL`: The maximum time a connection to the postgres server should be kept alive.
- `SHARE_POSTGRES_CONN_MAX`: The maximum number of connections to the postgres server.
Expand Down
14 changes: 13 additions & 1 deletion app/Env.hs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ withEnv action = do
githubClientID <- fromEnv "SHARE_GITHUB_CLIENTID" (pure . Right . Text.pack)
githubClientSecret <- fromEnv "SHARE_GITHUB_CLIENT_SECRET" (pure . Right . Text.pack)
hs256Key <- fromEnv "SHARE_HMAC_KEY" (pure . Right . BS.pack)
edDSAKey <- fromEnv "SHARE_EDDSA_KEY" (pure . Right . BS.pack)
zendeskAPIUser <- fromEnv "SHARE_ZENDESK_API_USER" (pure . Right . BS.pack)
zendeskAPIToken <- fromEnv "SHARE_ZENDESK_API_TOKEN" (pure . Right . BS.pack)
let zendeskAuth = Servant.BasicAuthData zendeskAPIUser zendeskAPIToken
Expand Down Expand Up @@ -88,7 +89,18 @@ withEnv action = do
| otherwise = Nothing
in r {Redis.connectTLSParams = tlsParams}
let acceptedAudiences = Set.singleton apiOrigin
let jwtSettings = JWT.defaultJWTSettings hs256Key acceptedAudiences apiOrigin
let legacyKey = JWT.KeyDescription {JWT.key = hs256Key, JWT.alg = JWT.HS256}
let signingKey = JWT.KeyDescription {JWT.key = edDSAKey, JWT.alg = JWT.Ed25519}
hashJWTJWK <- case JWT.keyDescToJWK legacyKey of
Left err -> throwIO err
Right (_thumbprint, jwk) -> pure jwk
-- I explicitly add the legacy key to the validation keys, so that the thumbprinted
-- version of the key is used for validation, which is needed for HashJWTs which are signed
-- with a 'kid'.
let validationKeys = Set.fromList [legacyKey]
jwtSettings <- case JWT.defaultJWTSettings signingKey (Just legacyKey) validationKeys acceptedAudiences apiOrigin of
Left cryptoError -> throwIO cryptoError
Right settings -> pure settings
let cookieSettings = Cookies.defaultCookieSettings Deployment.onLocal (Just (realToFrac cookieSessionTTL))
let sessionCookieKey = tShow Deployment.deployment <> "-share-session"
redisConnection <- Redis.checkedConnect redisConfig
Expand Down
3 changes: 2 additions & 1 deletion docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ services:
- SHARE_SERVER_PORT=5424
- SHARE_REDIS=redis://redis:6379
- SHARE_POSTGRES=postgresql://postgres:sekrit@postgres:5432
- SHARE_HMAC_KEY=test-key-test-key-test-key-test-key-
- SHARE_HMAC_KEY=hmac-key-test-key-test-key-test-
- SHARE_EDDSA_KEY=eddsa-key-test-key-test-key-test
- SHARE_DEPLOYMENT=local
- SHARE_POSTGRES_CONN_TTL=30
- SHARE_POSTGRES_CONN_MAX=10
Expand Down
3 changes: 2 additions & 1 deletion local.env
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ export SHARE_REDIS=redis://localhost:6379
export SHARE_POSTGRES=postgresql://postgres:sekrit@localhost:5432
export SHARE_POSTGRES_CONN_TTL=30
export SHARE_POSTGRES_CONN_MAX=10
export SHARE_HMAC_KEY="test-key-test-key-test-key-test-key-"
export SHARE_HMAC_KEY="hmac-key-test-key-test-key-test-"
export SHARE_EDDSA_KEY="eddsa-key-test-key-test-key-test"
export SHARE_SHARE_UI_ORIGIN="http://localhost:1234"
export SHARE_CLOUD_UI_ORIGIN="http://localhost:5678"
export SHARE_HOMEPAGE_ORIGIN="http://localhost:1111"
Expand Down
57 changes: 33 additions & 24 deletions share-auth/example/src/Lib.hs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import Data.Set qualified as Set
import Data.Text (Text)
import Data.Time (DiffTime)
import Database.Redis qualified as R
import GHC.Stack (HasCallStack)
import Network.URI qualified as URI
import Network.Wai.Handler.Warp qualified as Warp
import Servant
import Share.JWT qualified as JWT
import Share.OAuth.API (ServiceProviderAPI)
import Share.OAuth.IdentityProvider.Share qualified as Share
Expand All @@ -18,10 +22,6 @@ import Share.OAuth.ServiceProvider qualified as Auth
import Share.OAuth.Session (AuthCheckCtx, AuthenticatedUserId, MaybeAuthenticatedUserId, addAuthCheckCtx)
import Share.OAuth.Types (OAuthClientId (..), OAuthClientSecret (OAuthClientSecret), RedirectReceiverErr, UserId)
import Share.Utils.Servant.Cookies qualified as Cookies
import GHC.Stack (HasCallStack)
import Network.URI qualified as URI
import Network.Wai.Handler.Warp qualified as Warp
import Servant
import UnliftIO

-- | An example application endpoint which is optionally authenticated.
Expand All @@ -44,30 +44,30 @@ type MyAPI =
:<|> "error" :> ErrorEndpoint

-- | A handler which checks if the user is authenticated.
mayAuthedEndpoint :: MonadIO m => Maybe UserId -> m String
mayAuthedEndpoint :: (MonadIO m) => Maybe UserId -> m String
mayAuthedEndpoint mayCallerUserId = do
case mayCallerUserId of
Nothing -> pure "no user"
Just userId -> do
pure $ "Hello, " <> show userId

-- | A handler which requires an authenticated user.
authedEndpoint :: MonadIO m => UserId -> m String
authedEndpoint :: (MonadIO m) => UserId -> m String
authedEndpoint callerUserId = do
pure $ "Hello, " <> show callerUserId

-- | A handler which displays errors from the OAuth2 flow.
errorEndpoint :: Applicative m => Maybe String -> m String
errorEndpoint :: (Applicative m) => Maybe String -> m String
errorEndpoint err = do
pure $ fromMaybe "no error" err

-- | A helper function for constructing URIs from constant strings.
unsafeURI :: HasCallStack => String -> URI.URI
unsafeURI :: (HasCallStack) => String -> URI.URI
unsafeURI = fromJust . URI.parseURI

-- | A session callback which redirects the user to either an error page
-- or the authed handler endpoint depending on whether the oauth2 login succeeds.
mySessionCallback :: Applicative m => Either RedirectReceiverErr SessionCallbackData -> m URI
mySessionCallback :: (Applicative m) => Either RedirectReceiverErr SessionCallbackData -> m URI
mySessionCallback (Left err) = pure . fromJust . URI.parseURI $ "http://cloud:3030/error?error=" <> show err
mySessionCallback (Right _session) = pure $ unsafeURI "http://cloud:3030/authed"

Expand All @@ -78,7 +78,12 @@ main = do
redisConn <- R.checkedConnect R.defaultConnectInfo
putStrLn "booting up"

Warp.run 3030 $ serveWithContext (Proxy @MyAPI) ctx (myServer redisConn)
jwtSettings <- case JWT.defaultJWTSettings signingKey (Just legacyKey) rotatedKeys acceptedAudiences issuer of
Left cryptoError -> throwIO cryptoError
Right jwtS -> do
pure jwtS

Warp.run 3030 $ serveWithContext (Proxy @MyAPI) (ctx jwtSettings) (myServer redisConn jwtSettings)
putStrLn "exiting"
pure ()
where
Expand All @@ -87,18 +92,18 @@ main = do
apiProxy :: Proxy MyAPI
apiProxy = Proxy
-- The api context required by servant-auth
appCtx :: (Context '[Cookies.CookieSettings, JWT.JWTSettings])
appCtx = cookieSettings :. jwtSettings :. EmptyContext
appCtx :: JWT.JWTSettings -> (Context '[Cookies.CookieSettings, JWT.JWTSettings])
appCtx jwtSettings = cookieSettings :. jwtSettings :. EmptyContext
sessionCookieKey :: Text
sessionCookieKey = "session"
ctx :: Context (AuthCheckCtx .++ '[Cookies.CookieSettings, JWT.JWTSettings])
ctx = addAuthCheckCtx cookieSettings jwtSettings "session" appCtx
serviceProviderEndpoints :: ServerT ServiceProviderAPI R.Redis
serviceProviderEndpoints = Auth.serviceProviderServer Share.localShareIdentityProvider spConfig mySessionCallback
myServer :: R.Connection -> Server MyAPI
myServer conn =
ctx :: JWT.JWTSettings -> Context (AuthCheckCtx .++ '[Cookies.CookieSettings, JWT.JWTSettings])
ctx jwtSettings = addAuthCheckCtx cookieSettings jwtSettings "session" (appCtx jwtSettings)
serviceProviderEndpoints :: JWT.JWTSettings -> ServerT ServiceProviderAPI R.Redis
serviceProviderEndpoints jwtSettings = Auth.serviceProviderServer Share.localShareIdentityProvider (spConfig jwtSettings) mySessionCallback
myServer :: R.Connection -> JWT.JWTSettings -> Server MyAPI
myServer conn jwtSettings =
Servant.hoistServerWithContext apiProxy ctxProxy (unRedis conn) $
serviceProviderEndpoints
serviceProviderEndpoints jwtSettings
:<|> mayAuthedEndpoint
:<|> authedEndpoint
:<|> errorEndpoint
Expand All @@ -108,10 +113,8 @@ main = do
cookieDefaultTTL = Just $ 60 * 60 * 24 * 7 -- 1 week
cookieSettings :: Cookies.CookieSettings
cookieSettings = Cookies.defaultCookieSettings onLocal cookieDefaultTTL
jwtSettings :: JWT.JWTSettings
jwtSettings = JWT.defaultJWTSettings hs256Key acceptedAudiences issuer
spConfig :: ServiceProviderConfig
spConfig =
spConfig :: JWT.JWTSettings -> ServiceProviderConfig
spConfig jwtSettings =
ServiceProviderConfig
{ cookieSettings,
jwtSettings = jwtSettings,
Expand All @@ -124,7 +127,13 @@ main = do
sessionCookieKey
}
onLocal = True
hs256Key = "gpeakbroleymbscyqzrcalpemrjayhur"
-- Ensure you use cryptographically secure 32-byte keys in production use.
-- And don't re-use keys.
hs256Key = "example-32-byte-hs256Key-jayhuxr"
edDSAKey = "example-32-byte-edDSAKey-dxencne"
legacyKey = JWT.KeyDescription {JWT.key = hs256Key, JWT.alg = JWT.HS256}
signingKey = JWT.KeyDescription {JWT.key = edDSAKey, JWT.alg = JWT.Ed25519}
rotatedKeys = Set.empty
api = unsafeURI "http://cloud:3030"
serviceAudience = api
acceptedAudiences = Set.singleton serviceAudience
Expand Down
3 changes: 2 additions & 1 deletion share-auth/package.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,14 @@ dependencies:
- mtl
- transformers
- aeson
- base64-bytestring
- binary
- binary-instances
- bytestring
- case-insensitive
- containers
- cookie
- cryptonite
- crypton
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

crypton is where cryptonite moved to since HVR is no longer maintaining it.

- share-utils
- hasql
- hasql-interpolate
Expand Down
5 changes: 3 additions & 2 deletions share-auth/share-auth.cabal
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
cabal-version: 1.12

-- This file has been generated from package.yaml by hpack version 0.36.0.
-- This file has been generated from package.yaml by hpack version 0.37.0.
--
-- see: https://github.com/sol/hpack

Expand Down Expand Up @@ -73,13 +73,14 @@ library
MonadRandom
, aeson
, base >=4.7 && <5
, base64-bytestring
, binary
, binary-instances
, bytestring
, case-insensitive
, containers
, cookie
, cryptonite
, crypton
, hasql
, hasql-interpolate
, hedis
Expand Down
Loading
Loading