diff --git a/CHANGELOG.md b/CHANGELOG.md index a5aa1b9..7e6367c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.1.3.11] - 2022-02-06 +### Added +- Custom `swagger-ui` wrapper that allows to use BIOCAD's OIDC authorization from the UI. +- Compatibility with `openapi3` 3.2.0 and higher. + ## [0.1.3.10] - 2021-08-03 ### Added - Create OIDC config with provided tls manager. diff --git a/index.html.tmpl b/index.html.tmpl new file mode 100644 index 0000000..fa3ca93 --- /dev/null +++ b/index.html.tmpl @@ -0,0 +1,88 @@ + + + + + + Swagger UI + + + + + + + + + + +
+ + + + + + diff --git a/src/Web/Template/Servant/Auth.hs b/src/Web/Template/Servant/Auth.hs index b276acc..58f2651 100644 --- a/src/Web/Template/Servant/Auth.hs +++ b/src/Web/Template/Servant/Auth.hs @@ -2,6 +2,7 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TemplateHaskell #-} module Web.Template.Servant.Auth ( CbdAuth @@ -12,12 +13,13 @@ module Web.Template.Servant.Auth , oidcCfgWithManager , OIDCUser (..) , Permit + , swaggerSchemaUIBCDServer ) where -- after https://www.stackage.org/haddock/lts-15.15/servant-server-0.16.2/src/Servant.Server.Experimental.Auth.html import Control.Applicative ((<|>)) -import Control.Lens (At (at), ix, (&), (.~), (<&>), (?~), (^..), (^?)) +import Control.Lens (Iso', at, coerced, ix, (&), (.~), (<&>), (?~), (^..), (^?)) import Control.Monad.Except (runExceptT, unless) import Control.Monad.IO.Class (MonadIO, liftIO) import Data.IORef (readIORef, writeIORef) @@ -34,18 +36,23 @@ import Crypto.JWT (ClaimsSet, JWTError, JWTValidat SignedJWT, audiencePredicate, decodeCompact, defaultJWTValidationSettings, issuerPredicate, string, unregisteredClaims, uri, verifyClaims) +import Data.Aeson (Value) import Data.Aeson.Lens (AsPrimitive (_String), key, values) import Data.ByteString (ByteString, stripPrefix) import qualified Data.ByteString.Lazy as LB import Data.Cache (Cache) import qualified Data.Cache as Cache +import Data.OpenApi (Definitions, OpenApi, URL (..)) import Data.OpenApi.Internal (ApiKeyLocation (..), ApiKeyParams (..), - HttpSchemeType (..), SecurityRequirement (..), - SecurityScheme (..), SecuritySchemeType (..)) + HttpSchemeType (..), SecurityDefinitions (..), + SecurityRequirement (..), SecurityScheme (..), + SecuritySchemeType (..)) import Data.OpenApi.Lens (components, description, security, securitySchemes) import Data.OpenApi.Operation (allOperations, setResponse) +import qualified Data.Text as T import Data.Time.Clock (NominalDiffTime, UTCTime, addUTCTime, diffUTCTime, getCurrentTime, nominalDiffTimeToSeconds) +import FileEmbedLzma (embedText) import Network.HTTP.Client (Manager, httpLbs) import Network.HTTP.Client.TLS (newTlsManager) import Network.HTTP.Types.Header (hContentType) @@ -58,6 +65,8 @@ import Servant.Server (HasContextEntry (getContextEntr ServerError (..), err401, err403, err500) import Servant.Server.Internal (DelayedIO, addAuthCheck, delayedFailFatal, withRequest) +import Servant.Swagger.UI (SwaggerSchemaUI', swaggerUiFiles) +import Servant.Swagger.UI.Core (swaggerSchemaUIServerImpl) import System.Clock (TimeSpec (..)) import Web.Cookie (parseCookiesText) @@ -103,7 +112,7 @@ instance HasServer api context => HasServer (CbdAuth :> api) context where instance HasOpenApi api => HasOpenApi (CbdAuth :> api) where toOpenApi _ = toOpenApi @api Proxy - & components . securitySchemes . at "cbdCookie" ?~ idCookie + & components . securitySchemes . securityDefinitions . at "cbdCookie" ?~ idCookie & allOperations . security .~ [SecurityRequirement $ mempty & at "cbdCookie" ?~ []] & setResponse 401 (return $ mempty & description .~ "Authorization failed") where @@ -254,8 +263,6 @@ instance ( HasServer api context (die ERROR unauth500) (uncurry discoSuccess) where - appWellKnown u@URI {..} = u {uriPath = uriPath <> "/.well-known/openid-configuration"} - discoSuccess disco mbDiscoExp = liftIO $ do now <- getCurrentTime Cache.insert' oidcDiscoCache (expiration now mbDiscoExp oidcDefaultExpiration) () disco @@ -292,13 +299,22 @@ instance ( HasServer api context instance HasOpenApi api => HasOpenApi (OIDCAuth :> api) where toOpenApi _ = toOpenApi @api Proxy - & components . securitySchemes . at "cbdJWT" ?~ idJWT - & allOperations . security .~ [SecurityRequirement $ mempty & at "cbdJWT" ?~ []] + & components . securitySchemes . securityDefinitions . at "cbdJWT" ?~ idJWT + & components . securitySchemes . securityDefinitions . at "cbdOIDC" ?~ idOIDC + & allOperations . security .~ + [ SecurityRequirement $ mempty & at "cbdJWT" ?~ [] + , SecurityRequirement $ mempty & at "cbdOIDC" ?~ [] + ] & setResponse 401 (return $ mempty & description .~ "Authorization failed") where idJWT = SecurityScheme (SecuritySchemeHttp $ HttpSchemeBearer $ Just "jwt") (Just "jwt token") + idOIDC = SecurityScheme + -- It is expected that client will update this field in runtime before serving + -- generated swagegr, as there is no easy way to pass it to this instance. + (SecuritySchemeOpenIdConnect $ URL "NOT SET") + (Just "BIOCAD's OpenID Connect auth") -- | Adds authenthication via roles -- @@ -378,3 +394,34 @@ unauth500 = delayedFailFatal $ err500 { errBody = "{\"error\": \"Internal server error\"}" , errHeaders = [(hContentType, "application/json")] } + +swaggerUiIndexBCDTemplate :: Text +swaggerUiIndexBCDTemplate = $(embedText "index.html.tmpl") + +-- | Version of 'Servant.Swagger.UI.swaggerSchemaUIServer' that uses +-- our @index.html@ template to enable PKCE auth flow and prefill +-- OpenID client id. +swaggerSchemaUIBCDServer + :: Monad m + => ServerT api m ~ m Value + => OIDCConfig + -> OpenApi + -> ServerT (SwaggerSchemaUI' dir api) m +swaggerSchemaUIBCDServer oidcConfig openapi = + swaggerSchemaUIServerImpl swaggerUiIndexTemplateFilled swaggerUiFiles openapiWithOidcUrl + where + swaggerUiIndexTemplateFilled = + T.replace "BIOCAD_OIDC_CLIENT_ID" (oidcClientId oidcConfig) + swaggerUiIndexBCDTemplate + openapiWithOidcUrl = openapi + & components . securitySchemes . securityDefinitions . at "cbdOIDC" ?~ + SecurityScheme + (SecuritySchemeOpenIdConnect $ URL $ T.pack $ show $ appWellKnown $ oidcIssuer oidcConfig) + (Just "BIOCAD's OpenID Connect auth") + +-- | Append OIDC's @.well-known/openid-configuration" part to the base of OIDC issuer URI. +appWellKnown :: URI -> URI +appWellKnown u@URI {..} = u {uriPath = uriPath <> "/.well-known/openid-configuration"} + +securityDefinitions :: Iso' SecurityDefinitions (Definitions SecurityScheme) +securityDefinitions = coerced diff --git a/web-template.cabal b/web-template.cabal index e85cedc..6467e43 100644 --- a/web-template.cabal +++ b/web-template.cabal @@ -1,5 +1,5 @@ name: web-template -version: 0.1.3.10 +version: 0.1.3.11 synopsis: Web template description: Web template includes: @@ -15,7 +15,9 @@ maintainer: neterebskiy@biocad.ru copyright: (c) 2017, BIOCAD CBD category: Web build-type: Simple -extra-source-files: README.md +extra-source-files: + README.md + index.html.tmpl cabal-version: >=1.10 library @@ -45,6 +47,7 @@ library , cookie , data-default , fast-logger + , file-embed-lzma , generic-override >= 0.2.0.0 , generic-override-aeson , http-client @@ -55,14 +58,15 @@ library , lens-aeson , mtl , network-uri - , openapi3 >= 3.1.0 + , openapi3 >= 3.2.0 , openid-connect >= 0.1.1 , resourcet , scotty , servant >= 0.18 , servant-openapi3 , servant-server >= 0.18 - , servant-swagger-ui >= 0.3.5 + , servant-swagger-ui >= 0.3.5.4.5.0 + , servant-swagger-ui-core >= 0.3.5 , text , time , time