Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for jwt authorization (close #186) #255

Merged
merged 21 commits into from
Aug 30, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
1d82191
JWT Auth mode for server
ecthiender Jul 27, 2018
8f9b3c4
implement JWT specific errors
ecthiender Jul 30, 2018
556c4f5
Improve auth mode types
ecthiender Jul 30, 2018
1ba7d1e
Fix an issue parsing x-hasura-* claims from JWT
ecthiender Jul 30, 2018
e1fbf50
add support for RSA JWK; add JWT secret as JSON
ecthiender Jul 31, 2018
ec495d6
support RSA JWKs in PKCS8/PKCS1/X509 format
ecthiender Aug 3, 2018
6c1b6b9
Merge branch 'master' of github.com:hasura/graphql-engine into fix-18…
ecthiender Aug 6, 2018
bc90347
minor refactor and fix help text for jwt secret
ecthiender Aug 6, 2018
4511d30
Merge branch 'master' of github.com:hasura/graphql-engine into fix-18…
ecthiender Aug 7, 2018
8b28a28
Merge branch 'master' of github.com:hasura/graphql-engine into fix-18…
ecthiender Aug 9, 2018
7bf4e0a
code review fix for JWT support
ecthiender Aug 9, 2018
c20a647
Merge branch 'master' of github.com:hasura/graphql-engine into fix-18…
ecthiender Aug 22, 2018
9c6ee83
bug fix in jwt metadata handling
ecthiender Aug 22, 2018
ea77dbf
add support for x-hasura-allowed-roles in JWT mode
ecthiender Aug 24, 2018
6f1f783
Merge branch 'master' of github.com:hasura/graphql-engine into fix-18…
ecthiender Aug 28, 2018
08515ac
minor refactor in jwt auth
ecthiender Aug 28, 2018
1b2ad21
default role, when using allowed roles, should come from the jwt claims
ecthiender Aug 28, 2018
312a645
Merge branch 'master' of github.com:hasura/graphql-engine into fix-18…
ecthiender Aug 29, 2018
d59e3f4
change access key and webhook to newtypes
ecthiender Aug 30, 2018
4f2a977
Merge branch 'master' into fix-186-jwt-auth
0x777 Aug 30, 2018
0d2326b
Merge branch 'master' into fix-186-jwt-auth
shahidhk Aug 30, 2018
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
Prev Previous commit
Next Next commit
support RSA JWKs in PKCS8/PKCS1/X509 format
  • Loading branch information
ecthiender committed Aug 6, 2018
commit ec495d6a10e52920ecbad71675cffb50c5ab43e9
2 changes: 2 additions & 0 deletions server/graphql-engine.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ library
, jose
, pem
, x509
, asn1-encoding
, asn1-types

-- Server related
, warp
Expand Down
86 changes: 68 additions & 18 deletions server/src-lib/Hasura/Server/Auth/JWT.hs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,20 @@ import Control.Lens
import Crypto.JOSE.Types (Base64Integer (..))
import Crypto.JWT
import Crypto.PubKey.RSA (PublicKey (..))
import Data.ASN1.BinaryEncoding (DER (..))
import Data.ASN1.Encoding (decodeASN1')
import Data.ASN1.Types (ASN1 (End, IntVal, Start),
ASN1ConstructionType (Sequence),
fromASN1)
import Data.List (find)
import Data.Time.Clock (getCurrentTime)
--import Debug.Trace
import Hasura.Prelude
import Hasura.RQL.Types
import Hasura.Server.Utils (userRoleHeader)

import qualified Data.Aeson as A
-- import qualified Data.ByteString.Base64 as B64
import qualified Data.ByteString.Lazy as BL
import qualified Data.ByteString.Lazy.Char8 as BLC
import qualified Data.CaseInsensitive as CI
Expand Down Expand Up @@ -111,7 +118,8 @@ data JWTConfig
, jcKey :: !JWK
} deriving (Show, Eq)

-- | Parse from a {"key": "RS256", "key": <bytestring>} to JWTConfig
-- | Parse from a json string like `{"type": "RS256", "key": <bytestring>}` to
-- | JWTConfig
instance A.FromJSON JWTConfig where

parseJSON = A.withObject "foo" $ \o -> do
Expand All @@ -135,39 +143,81 @@ instance A.FromJSON JWTConfig where

parseRsaKey ktype key = do
let res = fromRawPem (BL.fromStrict $ TE.encodeUtf8 key)
case res of
Left e -> invalidJwk ("Could not decode PEM: " <> T.unpack e)
Right a -> return $ JWTConfig ktype a
err e = "Could not decode PEM: " <> T.unpack e
either (invalidJwk . err) (return . JWTConfig ktype) res

invalidJwk msg = fail ("Invalid JWK: " <> msg)


-- | Helper functions to decode PEM bytestring to RSA public key

-- try PKCS first, then x509
fromRawPem :: BL.ByteString -> Either Text JWK
fromRawPem k = fromCertRaw k >>= certToJwk
fromRawPem bs = -- pubKeyToJwk <=< fromPkcsPem
case fromPkcsPem bs of
Right pk -> pubKeyToJwk pk
Left e ->
case fromX509Pem bs of
Right pk1 -> pubKeyToJwk pk1
Left e1 -> Left (e <> " " <> e1)

-- decode a PKCS1 or PKCS8 PEM to obtain the public key
fromPkcsPem :: BL.ByteString -> Either Text PublicKey
fromPkcsPem bs = do
pems <- fmapL T.pack $ PEM.pemParseLBS bs
pem <- getAtleastOnePem pems
res <- fmapL asn1ErrToText $ decodeASN1' DER $ PEM.pemContent pem
-- trace ("ASN1 decodes: " ++ show res) (return ())
case res of
-- PKCS#1 format
[Start Sequence, IntVal n, IntVal e, End Sequence] ->
return $ PublicKey (calculateSize n) n e
-- try and see if its a PKCS#8 format
asn1 -> do
(pub, []) <- fmapL T.pack $ fromASN1 asn1
case pub of
X509.PubKeyRSA pk -> return pk
_ -> Left "Could not decode RSA public key"
where
asn1ErrToText = T.pack . show

fromCertRaw :: BL.ByteString -> Either Text X509.Certificate
fromCertRaw s = do
-- decode a x509 certificate containing the RSA public key
fromX509Pem :: BL.ByteString -> Either Text PublicKey
fromX509Pem s = do
-- try to parse bytestring to a [PEM]
pems <- fmapL T.pack $ PEM.pemParseLBS s
-- fail if [PEM] is empty
pem <- getAtleastOnePem pems

-- decode the bytestring to a certificate
signedExactCert <- fmapL T.pack $ X509.decodeSignedCertificate $
PEM.pemContent pem
return $ X509.signedObject $ X509.getSigned signedExactCert
where
fmapL fn (Left e) = Left $ fn e
fmapL _ (Right x) = pure x
let cert = X509.signedObject $ X509.getSigned signedExactCert
pubKey = X509.certPubKey cert

getAtleastOnePem [] = Left "No pem found"
getAtleastOnePem (x:_) = Right x
case pubKey of
X509.PubKeyRSA pk -> return pk
_ -> Left "Could not decode RSA public key from x509 cert"

certToJwk :: X509.Certificate -> Either Text JWK
certToJwk cert = do
let X509.PubKeyRSA (PublicKey _ n e) = X509.certPubKey cert
jwk' = fromKeyMaterial $ RSAKeyMaterial $ rsaKeyParams n e

pubKeyToJwk :: PublicKey -> Either Text JWK
pubKeyToJwk (PublicKey _ n e) = do
let jwk' = fromKeyMaterial $ RSAKeyMaterial rsaKeyParams
return $ jwk' & jwkKeyOps .~ Just [Verify]
where
rsaKeyParams n e = RSAKeyParameters (Base64Integer n) (Base64Integer e) Nothing
rsaKeyParams = RSAKeyParameters (Base64Integer n) (Base64Integer e) Nothing


fmapL :: (a -> a') -> Either a b -> Either a' b
fmapL fn (Left e) = Left (fn e)
fmapL _ (Right x) = pure x

getAtleastOnePem :: [PEM.PEM] -> Either Text PEM.PEM
getAtleastOnePem [] = Left "No pem found"
getAtleastOnePem (x:_) = Right x

calculateSize :: Integer -> Int
calculateSize = go 1
where
go i n | 2 ^ (i * 8) > n = i
| otherwise = go (i + 1) n