Skip to content

Commit 3c5f4ad

Browse files
authored
Merge pull request #36 from unisoncomputing/eddsa
Support EdDSA jwts in the Share auth lib.
2 parents 2093ee0 + 760cf79 commit 3c5f4ad

File tree

15 files changed

+236
-76
lines changed

15 files changed

+236
-76
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ See `local.env` for example values which are used for local development.
8787
- `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.
8888
- `SHARE_REDIS`: The URL of the redis server.
8989
- `SHARE_POSTGRES`: The URL of the postgres server.
90-
- `SHARE_HMAC_KEY`: A secret key used for cryptographic signing. This should be at least 32 characters long.
90+
- `SHARE_HMAC_KEY`: A secret key used for cryptographic signing of HashJWTs. This should be at least 32 characters long.
91+
- `SHARE_EDDSA_KEY`: A secret key used for cryptographic signing of user sessions. This should be at least 32 characters long.
9192
- `SHARE_DEPLOYMENT`: The deployment environment. One of: `local`, `staging`, `prod`.
9293
- `SHARE_POSTGRES_CONN_TTL`: The maximum time a connection to the postgres server should be kept alive.
9394
- `SHARE_POSTGRES_CONN_MAX`: The maximum number of connections to the postgres server.

app/Env.hs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ withEnv action = do
5252
githubClientID <- fromEnv "SHARE_GITHUB_CLIENTID" (pure . Right . Text.pack)
5353
githubClientSecret <- fromEnv "SHARE_GITHUB_CLIENT_SECRET" (pure . Right . Text.pack)
5454
hs256Key <- fromEnv "SHARE_HMAC_KEY" (pure . Right . BS.pack)
55+
edDSAKey <- fromEnv "SHARE_EDDSA_KEY" (pure . Right . BS.pack)
5556
zendeskAPIUser <- fromEnv "SHARE_ZENDESK_API_USER" (pure . Right . BS.pack)
5657
zendeskAPIToken <- fromEnv "SHARE_ZENDESK_API_TOKEN" (pure . Right . BS.pack)
5758
let zendeskAuth = Servant.BasicAuthData zendeskAPIUser zendeskAPIToken
@@ -88,7 +89,18 @@ withEnv action = do
8889
| otherwise = Nothing
8990
in r {Redis.connectTLSParams = tlsParams}
9091
let acceptedAudiences = Set.singleton apiOrigin
91-
let jwtSettings = JWT.defaultJWTSettings hs256Key acceptedAudiences apiOrigin
92+
let legacyKey = JWT.KeyDescription {JWT.key = hs256Key, JWT.alg = JWT.HS256}
93+
let signingKey = JWT.KeyDescription {JWT.key = edDSAKey, JWT.alg = JWT.Ed25519}
94+
hashJWTJWK <- case JWT.keyDescToJWK legacyKey of
95+
Left err -> throwIO err
96+
Right (_thumbprint, jwk) -> pure jwk
97+
-- I explicitly add the legacy key to the validation keys, so that the thumbprinted
98+
-- version of the key is used for validation, which is needed for HashJWTs which are signed
99+
-- with a 'kid'.
100+
let validationKeys = Set.fromList [legacyKey]
101+
jwtSettings <- case JWT.defaultJWTSettings signingKey (Just legacyKey) validationKeys acceptedAudiences apiOrigin of
102+
Left cryptoError -> throwIO cryptoError
103+
Right settings -> pure settings
92104
let cookieSettings = Cookies.defaultCookieSettings Deployment.onLocal (Just (realToFrac cookieSessionTTL))
93105
let sessionCookieKey = tShow Deployment.deployment <> "-share-session"
94106
redisConnection <- Redis.checkedConnect redisConfig

docker/docker-compose.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ services:
5252
- SHARE_SERVER_PORT=5424
5353
- SHARE_REDIS=redis://redis:6379
5454
- SHARE_POSTGRES=postgresql://postgres:sekrit@postgres:5432
55-
- SHARE_HMAC_KEY=test-key-test-key-test-key-test-key-
55+
- SHARE_HMAC_KEY=hmac-key-test-key-test-key-test-
56+
- SHARE_EDDSA_KEY=eddsa-key-test-key-test-key-test
5657
- SHARE_DEPLOYMENT=local
5758
- SHARE_POSTGRES_CONN_TTL=30
5859
- SHARE_POSTGRES_CONN_MAX=10

local.env

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ export SHARE_REDIS=redis://localhost:6379
99
export SHARE_POSTGRES=postgresql://postgres:sekrit@localhost:5432
1010
export SHARE_POSTGRES_CONN_TTL=30
1111
export SHARE_POSTGRES_CONN_MAX=10
12-
export SHARE_HMAC_KEY="test-key-test-key-test-key-test-key-"
12+
export SHARE_HMAC_KEY="hmac-key-test-key-test-key-test-"
13+
export SHARE_EDDSA_KEY="eddsa-key-test-key-test-key-test"
1314
export SHARE_SHARE_UI_ORIGIN="http://localhost:1234"
1415
export SHARE_CLOUD_UI_ORIGIN="http://localhost:5678"
1516
export SHARE_HOMEPAGE_ORIGIN="http://localhost:1111"

share-auth/example/src/Lib.hs

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ import Data.Set qualified as Set
88
import Data.Text (Text)
99
import Data.Time (DiffTime)
1010
import Database.Redis qualified as R
11+
import GHC.Stack (HasCallStack)
12+
import Network.URI qualified as URI
13+
import Network.Wai.Handler.Warp qualified as Warp
14+
import Servant
1115
import Share.JWT qualified as JWT
1216
import Share.OAuth.API (ServiceProviderAPI)
1317
import Share.OAuth.IdentityProvider.Share qualified as Share
@@ -18,10 +22,6 @@ import Share.OAuth.ServiceProvider qualified as Auth
1822
import Share.OAuth.Session (AuthCheckCtx, AuthenticatedUserId, MaybeAuthenticatedUserId, addAuthCheckCtx)
1923
import Share.OAuth.Types (OAuthClientId (..), OAuthClientSecret (OAuthClientSecret), RedirectReceiverErr, UserId)
2024
import Share.Utils.Servant.Cookies qualified as Cookies
21-
import GHC.Stack (HasCallStack)
22-
import Network.URI qualified as URI
23-
import Network.Wai.Handler.Warp qualified as Warp
24-
import Servant
2525
import UnliftIO
2626

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

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

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

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

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

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

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

81-
Warp.run 3030 $ serveWithContext (Proxy @MyAPI) ctx (myServer redisConn)
81+
jwtSettings <- case JWT.defaultJWTSettings signingKey (Just legacyKey) rotatedKeys acceptedAudiences issuer of
82+
Left cryptoError -> throwIO cryptoError
83+
Right jwtS -> do
84+
pure jwtS
85+
86+
Warp.run 3030 $ serveWithContext (Proxy @MyAPI) (ctx jwtSettings) (myServer redisConn jwtSettings)
8287
putStrLn "exiting"
8388
pure ()
8489
where
@@ -87,18 +92,18 @@ main = do
8792
apiProxy :: Proxy MyAPI
8893
apiProxy = Proxy
8994
-- The api context required by servant-auth
90-
appCtx :: (Context '[Cookies.CookieSettings, JWT.JWTSettings])
91-
appCtx = cookieSettings :. jwtSettings :. EmptyContext
95+
appCtx :: JWT.JWTSettings -> (Context '[Cookies.CookieSettings, JWT.JWTSettings])
96+
appCtx jwtSettings = cookieSettings :. jwtSettings :. EmptyContext
9297
sessionCookieKey :: Text
9398
sessionCookieKey = "session"
94-
ctx :: Context (AuthCheckCtx .++ '[Cookies.CookieSettings, JWT.JWTSettings])
95-
ctx = addAuthCheckCtx cookieSettings jwtSettings "session" appCtx
96-
serviceProviderEndpoints :: ServerT ServiceProviderAPI R.Redis
97-
serviceProviderEndpoints = Auth.serviceProviderServer Share.localShareIdentityProvider spConfig mySessionCallback
98-
myServer :: R.Connection -> Server MyAPI
99-
myServer conn =
99+
ctx :: JWT.JWTSettings -> Context (AuthCheckCtx .++ '[Cookies.CookieSettings, JWT.JWTSettings])
100+
ctx jwtSettings = addAuthCheckCtx cookieSettings jwtSettings "session" (appCtx jwtSettings)
101+
serviceProviderEndpoints :: JWT.JWTSettings -> ServerT ServiceProviderAPI R.Redis
102+
serviceProviderEndpoints jwtSettings = Auth.serviceProviderServer Share.localShareIdentityProvider (spConfig jwtSettings) mySessionCallback
103+
myServer :: R.Connection -> JWT.JWTSettings -> Server MyAPI
104+
myServer conn jwtSettings =
100105
Servant.hoistServerWithContext apiProxy ctxProxy (unRedis conn) $
101-
serviceProviderEndpoints
106+
serviceProviderEndpoints jwtSettings
102107
:<|> mayAuthedEndpoint
103108
:<|> authedEndpoint
104109
:<|> errorEndpoint
@@ -108,10 +113,8 @@ main = do
108113
cookieDefaultTTL = Just $ 60 * 60 * 24 * 7 -- 1 week
109114
cookieSettings :: Cookies.CookieSettings
110115
cookieSettings = Cookies.defaultCookieSettings onLocal cookieDefaultTTL
111-
jwtSettings :: JWT.JWTSettings
112-
jwtSettings = JWT.defaultJWTSettings hs256Key acceptedAudiences issuer
113-
spConfig :: ServiceProviderConfig
114-
spConfig =
116+
spConfig :: JWT.JWTSettings -> ServiceProviderConfig
117+
spConfig jwtSettings =
115118
ServiceProviderConfig
116119
{ cookieSettings,
117120
jwtSettings = jwtSettings,
@@ -124,7 +127,13 @@ main = do
124127
sessionCookieKey
125128
}
126129
onLocal = True
127-
hs256Key = "gpeakbroleymbscyqzrcalpemrjayhur"
130+
-- Ensure you use cryptographically secure 32-byte keys in production use.
131+
-- And don't re-use keys.
132+
hs256Key = "example-32-byte-hs256Key-jayhuxr"
133+
edDSAKey = "example-32-byte-edDSAKey-dxencne"
134+
legacyKey = JWT.KeyDescription {JWT.key = hs256Key, JWT.alg = JWT.HS256}
135+
signingKey = JWT.KeyDescription {JWT.key = edDSAKey, JWT.alg = JWT.Ed25519}
136+
rotatedKeys = Set.empty
128137
api = unsafeURI "http://cloud:3030"
129138
serviceAudience = api
130139
acceptedAudiences = Set.singleton serviceAudience

share-auth/package.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,14 @@ dependencies:
6363
- mtl
6464
- transformers
6565
- aeson
66+
- base64-bytestring
6667
- binary
6768
- binary-instances
6869
- bytestring
6970
- case-insensitive
7071
- containers
7172
- cookie
72-
- cryptonite
73+
- crypton
7374
- share-utils
7475
- hasql
7576
- hasql-interpolate

share-auth/share-auth.cabal

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
cabal-version: 1.12
22

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

@@ -73,13 +73,14 @@ library
7373
MonadRandom
7474
, aeson
7575
, base >=4.7 && <5
76+
, base64-bytestring
7677
, binary
7778
, binary-instances
7879
, bytestring
7980
, case-insensitive
8081
, containers
8182
, cookie
82-
, cryptonite
83+
, crypton
8384
, hasql
8485
, hasql-interpolate
8586
, hedis

0 commit comments

Comments
 (0)