diff options
-rw-r--r-- | overlays/spm/frontend/src/app/spm/spm.component.ts | 4 | ||||
-rw-r--r-- | overlays/spm/lib/Spm/Api.hs | 31 | ||||
-rw-r--r-- | overlays/spm/package.yaml | 3 | ||||
-rw-r--r-- | overlays/spm/server/Crypto/JOSE/Error/Instances.hs | 11 | ||||
-rw-r--r-- | overlays/spm/server/Spm/Server.hs | 24 | ||||
-rw-r--r-- | overlays/spm/spm.nix | 4 |
6 files changed, 60 insertions, 17 deletions
diff --git a/overlays/spm/frontend/src/app/spm/spm.component.ts b/overlays/spm/frontend/src/app/spm/spm.component.ts index f6892d7c..361a1ce4 100644 --- a/overlays/spm/frontend/src/app/spm/spm.component.ts +++ b/overlays/spm/frontend/src/app/spm/spm.component.ts | |||
@@ -36,7 +36,7 @@ export interface ClaimedSpmMail { | |||
36 | } | 36 | } |
37 | 37 | ||
38 | export interface SpmJwtUnregisteredPayload { | 38 | export interface SpmJwtUnregisteredPayload { |
39 | local: string; | 39 | 'li.yggdrasil.local': string; |
40 | } | 40 | } |
41 | 41 | ||
42 | type SpmJwtPayload = JwtPayload & SpmJwtUnregisteredPayload; | 42 | type SpmJwtPayload = JwtPayload & SpmJwtUnregisteredPayload; |
@@ -136,7 +136,7 @@ export class SpmComponent implements OnInit, OnDestroy { | |||
136 | const curr: Map<string, SpmMail> = this.spmMails$.getValue(); | 136 | const curr: Map<string, SpmMail> = this.spmMails$.getValue(); |
137 | curr.set(k, { | 137 | curr.set(k, { |
138 | state: 'loaded', | 138 | state: 'loaded', |
139 | local: payload.local, | 139 | local: payload['li.yggdrasil.local'], |
140 | domain: payload.aud, | 140 | domain: payload.aud, |
141 | jwt: encoded, | 141 | jwt: encoded, |
142 | expiration: payload.exp ? new Date(1000 * payload.exp) : undefined, | 142 | expiration: payload.exp ? new Date(1000 * payload.exp) : undefined, |
diff --git a/overlays/spm/lib/Spm/Api.hs b/overlays/spm/lib/Spm/Api.hs index 04dff2c9..8285cc55 100644 --- a/overlays/spm/lib/Spm/Api.hs +++ b/overlays/spm/lib/Spm/Api.hs | |||
@@ -5,6 +5,7 @@ module Spm.Api | |||
5 | , SpmMailbox, SpmDomain | 5 | , SpmMailbox, SpmDomain |
6 | , SpmLocal(..), SpmExtension(..) | 6 | , SpmLocal(..), SpmExtension(..) |
7 | , SpmMapping(..), SpmMappingState(..), SpmMappingListingItem(..), SpmMappingListing(..) | 7 | , SpmMapping(..), SpmMappingState(..), SpmMappingListingItem(..), SpmMappingListing(..) |
8 | , SpmJWTClaims(..), _spmjwtLocal, _SpmJWTLocal | ||
8 | , _SpmMappingText, _SpmMappingStateReject | 9 | , _SpmMappingText, _SpmMappingStateReject |
9 | , spmMappingAncestors | 10 | , spmMappingAncestors |
10 | , SpmApi, spmApi | 11 | , SpmApi, spmApi |
@@ -24,18 +25,21 @@ import Type.Reflection (Typeable) | |||
24 | 25 | ||
25 | import Control.Lens | 26 | import Control.Lens |
26 | 27 | ||
28 | import Control.Monad (guard) | ||
29 | |||
27 | import Data.CaseInsensitive (CI) | 30 | import Data.CaseInsensitive (CI) |
28 | import qualified Data.CaseInsensitive as CI | 31 | import qualified Data.CaseInsensitive as CI |
29 | import Data.CaseInsensitive.Instances () | 32 | import Data.CaseInsensitive.Instances () |
30 | 33 | ||
31 | import Crypto.JOSE.JWK (JWKSet) | 34 | import Crypto.JOSE.JWK (JWKSet) |
32 | import Crypto.JWT (SignedJWT) | 35 | import Crypto.JWT (SignedJWT, ClaimsSet, HasClaimsSet(..), emptyClaimsSet) |
33 | import Crypto.JWT.Instances () | 36 | import Crypto.JWT.Instances () |
34 | 37 | ||
35 | import Data.UUID (UUID) | 38 | import Data.UUID (UUID) |
36 | import Data.UUID.Instances () | 39 | import Data.UUID.Instances () |
37 | 40 | ||
38 | import qualified Data.Aeson as JSON | 41 | import qualified Data.Aeson as JSON |
42 | import qualified Data.Aeson.Lens as JSON | ||
39 | import Data.Aeson.TH (deriveJSON) | 43 | import Data.Aeson.TH (deriveJSON) |
40 | import Data.Aeson.Casing | 44 | import Data.Aeson.Casing |
41 | 45 | ||
@@ -163,6 +167,31 @@ deriveJSON (aesonPrefix trainCase) ''SpmMappingListingItem | |||
163 | instance ToJSON SpmMappingListing where | 167 | instance ToJSON SpmMappingListing where |
164 | toJSON SpmMappingListing{..} = JSON.object [ "mappings" JSON..= unSpmMappingListing ] | 168 | toJSON SpmMappingListing{..} = JSON.object [ "mappings" JSON..= unSpmMappingListing ] |
165 | 169 | ||
170 | data SpmJWTClaims = SpmJWTClaims | ||
171 | { spmjwtStdClaims :: ClaimsSet | ||
172 | , spmjwtLocal :: SpmLocal | ||
173 | } deriving stock (Eq, Show, Generic, Typeable) | ||
174 | |||
175 | makeLensesFor [("spmjwtStdClaims", "_stdClaims"), ("spmjwtLocal", "_spmjwtLocal")] ''SpmJWTClaims | ||
176 | |||
177 | instance HasClaimsSet SpmJWTClaims where | ||
178 | claimsSet = _stdClaims | ||
179 | |||
180 | _SpmJWTLocal :: (Wrapped l, Unwrapped l ~ Unwrapped SpmLocal) => Prism' SpmJWTClaims l | ||
181 | _SpmJWTLocal = prism' toClaims fromClaims | ||
182 | where toClaims (view $ _Wrapped' . _Unwrapped' -> spmjwtLocal) = SpmJWTClaims{..} | ||
183 | where spmjwtStdClaims = emptyClaimsSet | ||
184 | fromClaims SpmJWTClaims{..} = view (_Wrapped' . _Unwrapped') spmjwtLocal <$ guard (spmjwtStdClaims == emptyClaimsSet) | ||
185 | |||
186 | instance JSON.ToJSON SpmJWTClaims where | ||
187 | toJSON SpmJWTClaims{..} = JSON.toJSON spmjwtStdClaims | ||
188 | & JSON._Object . at "li.yggdrasil.local" .~ Just (JSON.toJSON spmjwtLocal) | ||
189 | |||
190 | instance JSON.FromJSON SpmJWTClaims where | ||
191 | parseJSON = JSON.withObject "SpmJWTClaims" $ \o -> SpmJWTClaims | ||
192 | <$> JSON.parseJSON (JSON._Object # o) | ||
193 | <*> o JSON..: "li.yggdrasil.local" | ||
194 | |||
166 | 195 | ||
167 | type SpmApi = "whoami" :> Get '[PlainText, JSON] SpmMailbox | 196 | type SpmApi = "whoami" :> Get '[PlainText, JSON] SpmMailbox |
168 | :<|> "domain" :> Get '[PlainText, JSON] SpmDomain | 197 | :<|> "domain" :> Get '[PlainText, JSON] SpmDomain |
diff --git a/overlays/spm/package.yaml b/overlays/spm/package.yaml index e188dc77..6d4abe1c 100644 --- a/overlays/spm/package.yaml +++ b/overlays/spm/package.yaml | |||
@@ -1,5 +1,5 @@ | |||
1 | name: spm | 1 | name: spm |
2 | version: 0.1.0 | 2 | version: 0.1.1 |
3 | 3 | ||
4 | default-extensions: | 4 | default-extensions: |
5 | - NoImplicitPrelude | 5 | - NoImplicitPrelude |
@@ -36,6 +36,7 @@ library: | |||
36 | - lens | 36 | - lens |
37 | - case-insensitive | 37 | - case-insensitive |
38 | - aeson | 38 | - aeson |
39 | - lens-aeson | ||
39 | - jose | 40 | - jose |
40 | - uuid | 41 | - uuid |
41 | - containers | 42 | - containers |
diff --git a/overlays/spm/server/Crypto/JOSE/Error/Instances.hs b/overlays/spm/server/Crypto/JOSE/Error/Instances.hs new file mode 100644 index 00000000..9f3eea51 --- /dev/null +++ b/overlays/spm/server/Crypto/JOSE/Error/Instances.hs | |||
@@ -0,0 +1,11 @@ | |||
1 | {-# OPTIONS_GHC -Wno-orphans #-} | ||
2 | |||
3 | module Crypto.JOSE.Error.Instances () where | ||
4 | |||
5 | import Data.Monoid (Monoid) | ||
6 | import Control.Monad (Monad) | ||
7 | import Control.Applicative (Alternative) | ||
8 | |||
9 | import Crypto.JOSE.Error | ||
10 | |||
11 | deriving newtype instance (Monad m, Monoid e) => Alternative (JOSE e m) | ||
diff --git a/overlays/spm/server/Spm/Server.hs b/overlays/spm/server/Spm/Server.hs index df12e3d8..8e7f8786 100644 --- a/overlays/spm/server/Spm/Server.hs +++ b/overlays/spm/server/Spm/Server.hs | |||
@@ -44,7 +44,7 @@ import Database.Persist | |||
44 | import Database.Persist.Postgresql | 44 | import Database.Persist.Postgresql |
45 | import UnliftIO.Pool | 45 | import UnliftIO.Pool |
46 | 46 | ||
47 | import Control.Monad.Trans.Reader (ReaderT, runReaderT) | 47 | import Control.Monad.Trans.Reader (ReaderT, runReaderT, mapReaderT) |
48 | 48 | ||
49 | import Control.Monad.Logger | 49 | import Control.Monad.Logger |
50 | 50 | ||
@@ -58,12 +58,12 @@ import qualified Data.UUID as UUID | |||
58 | import qualified Data.UUID.V4 as UUID | 58 | import qualified Data.UUID.V4 as UUID |
59 | 59 | ||
60 | import qualified Data.Aeson as JSON | 60 | import qualified Data.Aeson as JSON |
61 | import Data.Aeson.Lens (_JSON) | ||
62 | 61 | ||
63 | import System.FilePath ((</>), isRelative) | 62 | import System.FilePath ((</>), isRelative) |
64 | 63 | ||
65 | import Crypto.JOSE.JWK hiding (Context) | 64 | import Crypto.JOSE.JWK hiding (Context) |
66 | import Crypto.JOSE.JWK.Instances () | 65 | import Crypto.JOSE.JWK.Instances () |
66 | import Crypto.JOSE.Error.Instances () | ||
67 | 67 | ||
68 | import Crypto.Random.Instances () | 68 | import Crypto.Random.Instances () |
69 | import qualified Crypto.Random as Crypto | 69 | import qualified Crypto.Random as Crypto |
@@ -197,10 +197,13 @@ mkSpmApp = do | |||
197 | spmSql :: ReaderT SqlBackend Handler' a -> Handler' a | 197 | spmSql :: ReaderT SqlBackend Handler' a -> Handler' a |
198 | spmSql act = do | 198 | spmSql act = do |
199 | sqlPool <- view sctxSqlPool | 199 | sqlPool <- view sctxSqlPool |
200 | withResource sqlPool $ runReaderT act | 200 | mapReaderT (mapLoggingT $ either throwError pure <=< liftIO) . withResource sqlPool $ mapReaderT (mapLoggingT runHandler) . runReaderT act |
201 | 201 | ||
202 | spmJWT :: forall error a. Show error => ServerError -> ExceptT error IO a -> Handler' a | 202 | spmJWT :: forall error a. Show error => ServerError -> JOSE error IO a -> Handler' a |
203 | spmJWT errTemplate = either (\err -> throwError errTemplate{ errBody = LBS.fromStrict . Text.encodeUtf8 . Text.pack $ show err }) return <=< liftIO . runExceptT | 203 | spmJWT errTemplate = either (\err -> throwError errTemplate{ errBody = LBS.fromStrict . Text.encodeUtf8 . Text.pack $ show err }) return <=< liftIO . runJOSE |
204 | |||
205 | withJOSE :: forall m e e' a. Functor m => (e -> e') -> JOSE e m a -> JOSE e' m a | ||
206 | withJOSE f = JOSE . withExceptT f . unwrapJOSE | ||
204 | 207 | ||
205 | generateLocal :: MonadIO m => SpmStyle -> m MailLocal | 208 | generateLocal :: MonadIO m => SpmStyle -> m MailLocal |
206 | generateLocal SpmWords = fmap (review _Wrapped . CI.mk) . liftIO $ do | 209 | generateLocal SpmWords = fmap (review _Wrapped . CI.mk) . liftIO $ do |
@@ -254,24 +257,23 @@ spmServer dom mbox = whoami | |||
254 | instanceId' <- view sctxInstanceId | 257 | instanceId' <- view sctxInstanceId |
255 | jwks <- view $ sctxJwkSet . _Wrapped | 258 | jwks <- view $ sctxJwkSet . _Wrapped |
256 | tokenId <- liftIO UUID.nextRandom | 259 | tokenId <- liftIO UUID.nextRandom |
257 | let claimsSet = emptyClaimsSet | 260 | let jwtClaims = (_SpmJWTLocal # local) |
258 | & claimIss ?~ (JWT.string # UUID.toText instanceId') | 261 | & claimIss ?~ (JWT.string # UUID.toText instanceId') |
259 | & claimAud ?~ Audience (pure $ dom ^. _Wrapped . to CI.original . re JWT.string) | 262 | & claimAud ?~ Audience (pure $ dom ^. _Wrapped . to CI.original . re JWT.string) |
260 | & claimNbf ?~ NumericDate t | 263 | & claimNbf ?~ NumericDate t |
261 | & claimIat ?~ NumericDate t | 264 | & claimIat ?~ NumericDate t |
262 | & claimExp ?~ NumericDate (600 `addUTCTime` t) | 265 | & claimExp ?~ NumericDate (600 `addUTCTime` t) |
263 | & claimJti ?~ UUID.toText tokenId | 266 | & claimJti ?~ UUID.toText tokenId |
264 | & unregisteredClaims . at "local" ?~ view (_Wrapped . re _JSON) local | ||
265 | spmJWT @JWT.Error err500 $ do | 267 | spmJWT @JWT.Error err500 $ do |
266 | (jwsAlg, selectedJwk) <- withExceptT (fromMaybe JWT.NoUsableKeys . getFirst) . asum $ map (\jwk' -> (, jwk') <$> withExceptT (First . Just) (bestJWSAlg jwk')) jwks | 268 | (jwsAlg, selectedJwk) <- withJOSE (fromMaybe JWT.NoUsableKeys . getFirst) . asum $ map (\jwk' -> (, jwk') <$> withJOSE (First . Just) (bestJWSAlg jwk')) jwks |
267 | signClaims selectedJwk (newJWSHeader ((), jwsAlg)) claimsSet | 269 | signJWT selectedJwk (newJWSHeader ((), jwsAlg)) jwtClaims |
268 | 270 | ||
269 | claim jwt = do | 271 | claim jwt = do |
270 | jwks <- view sctxJwkSet | 272 | jwks <- view sctxJwkSet |
271 | let validationSettings' = defaultJWTValidationSettings ((== Just dom) . fmap (review _Wrapped . CI.mk) . preview JWT.string) | 273 | let validationSettings' = defaultJWTValidationSettings ((== Just dom) . fmap (review _Wrapped . CI.mk) . preview JWT.string) |
272 | & jwtValidationSettingsAllowedSkew .~ 5 | 274 | & jwtValidationSettingsAllowedSkew .~ 5 |
273 | claims <- spmJWT @JWT.JWTError err403 $ verifyClaims validationSettings' jwks jwt | 275 | jwtClaims <- spmJWT @JWT.JWTError err403 $ verifyJWT validationSettings' jwks jwt |
274 | mailboxMappingLocal <- maybe (throwError err400{ errBody = "Claim ‘local’ missing" }) (return . Just) $ claims ^? unregisteredClaims . ix "local" . _JSON . to CI.mk . re _Wrapped | 276 | let mailboxMappingLocal = Just $ jwtClaims ^. _spmjwtLocal . _Wrapped . _Unwrapped |
275 | 277 | ||
276 | spmSql $ do | 278 | spmSql $ do |
277 | Entity mailboxMappingMailbox _ <- maybe (throwError err404) return <=< getBy $ UniqueMailbox mbox | 279 | Entity mailboxMappingMailbox _ <- maybe (throwError err404) return <=< getBy $ UniqueMailbox mbox |
diff --git a/overlays/spm/spm.nix b/overlays/spm/spm.nix index 7c9eb933..2d1f7eed 100644 --- a/overlays/spm/spm.nix +++ b/overlays/spm/spm.nix | |||
@@ -10,13 +10,13 @@ | |||
10 | }: | 10 | }: |
11 | mkDerivation { | 11 | mkDerivation { |
12 | pname = "spm"; | 12 | pname = "spm"; |
13 | version = "0.1.0"; | 13 | version = "0.1.1"; |
14 | src = ./.; | 14 | src = ./.; |
15 | isLibrary = true; | 15 | isLibrary = true; |
16 | isExecutable = true; | 16 | isExecutable = true; |
17 | libraryHaskellDepends = [ | 17 | libraryHaskellDepends = [ |
18 | aeson aeson-casing base case-insensitive containers jose lens | 18 | aeson aeson-casing base case-insensitive containers jose lens |
19 | servant text uuid | 19 | lens-aeson servant text uuid |
20 | ]; | 20 | ]; |
21 | libraryToolDepends = [ hpack ]; | 21 | libraryToolDepends = [ hpack ]; |
22 | executableHaskellDepends = [ | 22 | executableHaskellDepends = [ |