mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-10-26 10:20:54 +03:00
server: add support for ES-* ( ES256, ES384 and ES512) algorithms for signing the JWT
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/9273 GitOrigin-RevId: e891a14e992e4345f5470e1e99dbfc21d9105c31
This commit is contained in:
parent
d918e701a1
commit
1372a649df
@ -174,12 +174,13 @@ The JWT will normally also contain standard (`sub`, `iat` etc.) and custom (`nam
|
||||
### type {#jwt-json-type}
|
||||
|
||||
This specifies the cryptographic signing algorithm which is used to sign the JWTs. Valid values are : `HS256`, `HS384`,
|
||||
`HS512`, `RS256`, `RS384`, `RS512`, `Ed25519`. (see [https://jwt.io](https://jwt.io/)).
|
||||
`HS512`, `RS256`, `RS384`, `RS512`, `Ed25519`, `ES256`, `ES384`, `ES512`. (see [https://jwt.io](https://jwt.io/)).
|
||||
|
||||
`HS*` is for HMAC-SHA based algorithms. `RS*` is for RSA based signing. `Ed*` is for Edwards-curve Digital Signature
|
||||
algorithms. For example, if your auth server is using HMAC-SHA256 for signing the JWTs, then use `HS256`. If it is using
|
||||
RSA with SHA-512, then use `RS512`. If it is using an EdDSA instance of Edwards25519, then use `Ed25519`. EC public keys
|
||||
are not yet supported.
|
||||
algorithms. `ES*` is for Elliptic-curve Digital Signature algorithms. For example, if your auth server is using
|
||||
HMAC-SHA256 for signing the JWTs, then use `HS256`. If it is using RSA with SHA-512, then use `RS512`. If it is using an
|
||||
EdDSA instance of Edwards25519, then use `Ed25519`. If it is using an ES instance of ECDSA with 256-bit curve, then use
|
||||
`ES256`.
|
||||
|
||||
This is an optional field. This is required only if you are using the `key` property in the config.
|
||||
|
||||
@ -187,8 +188,8 @@ This is an optional field. This is required only if you are using the `key` prop
|
||||
|
||||
- In the case of a symmetric key (i.e. a HMAC-based key), just the key as is. (e.g. -"abcdef..."). The key must be long
|
||||
enough for the chosen algorithm, (e.g. for HS256 it must be at least 32 characters long).
|
||||
- In the case of an asymmetric key (RSA, EdDSA etc.), only the **public** key, in a PEM-encoded string or as an X509
|
||||
certificate.
|
||||
- In the case of an asymmetric key (RSA, EdDSA, ECDSA etc.), only the **public** key, in a PEM-encoded string or as an
|
||||
X509 certificate.
|
||||
|
||||
This is an optional field. You can also provide a URL to fetch JWKs from using the `jwk_url` field.
|
||||
|
||||
@ -668,8 +669,38 @@ the public key.
|
||||
```json
|
||||
{
|
||||
"type":"Ed25519",
|
||||
"key": "-----BEGIN CERTIFICATE REQUEST-----\nMIIBAzCBtgIBADAnMQswCQYDVQQGEwJERTEYMBYGA1UEAwwPd3d3LmV4YW1wbGUu\nY29tMCowBQYDK2VwAyEA/9DV/InajW02Q0tC/tyr9mCSbSnNP1txICXVJrTGKDSg\nXDBaBgkqhkiG9w0BCQ4xTTBLMAsGA1UdDwQEAwIEMDATBgNVHSUEDDAKBggrBgEF\nBQcDATAnBgNVHREEIDAegg93d3cuZXhhbXBsZS5jb22CC2V4YW1wbGUuY29tMAUG\nAytlcANBAKbTqnTyPcf4ZkVuq2tC108pBGY19VgyoI+PP2wD2KaRz4QAO7Bjd+7S\nljyJoN83UDdtdtgb7aFgb611gx9W4go=\n-----END CERTIFICATE REQUEST-----
|
||||
"
|
||||
"key": "-----BEGIN CERTIFICATE REQUEST-----\nMIIBAzCBtgIBADAnMQswCQYDVQQGEwJERTEYMBYGA1UEAwwPd3d3LmV4YW1wbGUu\nY29tMCowBQYDK2VwAyEA/9DV/InajW02Q0tC/tyr9mCSbSnNP1txICXVJrTGKDSg\nXDBaBgkqhkiG9w0BCQ4xTTBLMAsGA1UdDwQEAwIEMDATBgNVHSUEDDAKBggrBgEF\nBQcDATAnBgNVHREEIDAegg93d3cuZXhhbXBsZS5jb22CC2V4YW1wbGUuY29tMAUG\nAytlcANBAKbTqnTyPcf4ZkVuq2tC108pBGY19VgyoI+PP2wD2KaRz4QAO7Bjd+7S\nljyJoN83UDdtdtgb7aFgb611gx9W4go=\n-----END CERTIFICATE REQUEST-----"
|
||||
}
|
||||
```
|
||||
|
||||
#### EC based
|
||||
|
||||
If your auth server is using ECDSA to sign JWTs, and is using the ES variant with a 256-bit key, the JWT config only
|
||||
needs to have the public key.
|
||||
|
||||
**Example 1**: public key in PEM format (not OpenSSH format):
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "ES256",
|
||||
"key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEVs/o5+uQbTjL3chynL4wXgUg2R9\nq9UU8I5mEovUf86QZ7kOBIjJwqnzD1omageEHWwHdBO6B+dFabmdT9POxg==\n-----END PUBLIC KEY-----"
|
||||
}
|
||||
```
|
||||
|
||||
**Example 2**: public key as X509 certificate:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "ES256",
|
||||
"key": "-----BEGIN CERTIFICATE-----\nMIIBbjCCARWgAwIBAgIUGn02F6Y6s88dDGmIfwiNxWxDjhswCgYIKoZIzj0EAwIw\nDTELMAkGA1UEBhMCSU4wHhcNMjMwNTI0MTAzNTI4WhcNMjgwNTIyMTAzNTI4WjAN\nMQswCQYDVQQGEwJJTjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABBFbP6OfrkG0\n4y93Icpy+MF4FINkfavVFPCOZhKL1H/OkGe5DgSIycKp8w9aJmoHhB1sB3QTugfn\nRWm5nU/TzsajUzBRMB0GA1UdDgQWBBSaqFjzps1qG+x2DPISjaXTWsTOdDAfBgNV\nHSMEGDAWgBSaqFjzps1qG+x2DPISjaXTWsTOdDAPBgNVHRMBAf8EBTADAQH/MAoG\nCCqGSM49BAMCA0cAMEQCIBDHHWa/uLAVdGFEk82auTmw995+MsRwv52VXLw2Z+ji\nAiAXzOWIcGN8p25uhUN/7v9gEcADGIS4yUiv8gsn/Jk2ow==\n-----END CERTIFICATE-----"
|
||||
}
|
||||
```
|
||||
|
||||
**Example 3**: public key published as JWKs:
|
||||
|
||||
```json
|
||||
{
|
||||
"jwk_url": "https://www.gstatic.com/iap/verify/public_key-jwk"
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -91,7 +91,7 @@ import Hasura.HTTP
|
||||
import Hasura.Logging (Hasura, LogLevel (..), Logger (..))
|
||||
import Hasura.Prelude
|
||||
import Hasura.RQL.Types.Roles (RoleName, mkRoleName)
|
||||
import Hasura.Server.Auth.JWT.Internal (parseEdDSAKey, parseHmacKey, parseRsaKey)
|
||||
import Hasura.Server.Auth.JWT.Internal (parseEdDSAKey, parseEsKey, parseHmacKey, parseRsaKey)
|
||||
import Hasura.Server.Auth.JWT.Logging
|
||||
import Hasura.Server.Utils
|
||||
( executeJSONPath,
|
||||
@ -837,7 +837,10 @@ instance J.FromJSON JWTConfig where
|
||||
"RS384" -> runEither $ parseRsaKey rawKey
|
||||
"RS512" -> runEither $ parseRsaKey rawKey
|
||||
"Ed25519" -> runEither $ parseEdDSAKey rawKey
|
||||
-- TODO(from master): support ES256, ES384, ES512, PS256, PS384, Ed448 (JOSE doesn't support it as of now)
|
||||
"ES256" -> runEither $ parseEsKey rawKey
|
||||
"ES384" -> runEither $ parseEsKey rawKey
|
||||
"ES512" -> runEither $ parseEsKey rawKey
|
||||
-- TODO(from master): support PS256, PS384, Ed448 (JOSE doesn't support it as of now)
|
||||
_ -> invalidJwk ("Key type: " <> T.unpack keyType <> " is not supported")
|
||||
|
||||
runEither = either (invalidJwk . T.unpack) return
|
||||
|
@ -2,6 +2,7 @@ module Hasura.Server.Auth.JWT.Internal
|
||||
( parseEdDSAKey,
|
||||
parseHmacKey,
|
||||
parseRsaKey,
|
||||
parseEsKey,
|
||||
)
|
||||
where
|
||||
|
||||
@ -46,6 +47,12 @@ parseEdDSAKey key = do
|
||||
err e = "Could not decode PEM: " <> e
|
||||
onLeft res (Left . err)
|
||||
|
||||
parseEsKey :: Text -> Either Text JWK
|
||||
parseEsKey key = do
|
||||
let res = fromRawPem (unUTF8 $ fromText key)
|
||||
err e = "Could not decode PEM: " <> e
|
||||
onLeft res (Left . err)
|
||||
|
||||
-- | Helper functions to decode PEM bytestring to RSA public key
|
||||
|
||||
-- try PKCS first, then x509
|
||||
@ -92,7 +99,8 @@ fromX509Pem s = do
|
||||
case pubKey of
|
||||
X509.PubKeyRSA pk -> return $ X509.PubKeyRSA pk
|
||||
X509.PubKeyEd25519 pk -> return $ X509.PubKeyEd25519 pk
|
||||
_ -> Left "Could not decode RSA or EdDSA public key from x509 cert"
|
||||
X509.PubKeyEC pk -> return $ X509.PubKeyEC pk
|
||||
_ -> Left "Could not decode RSA, EdDSA or EC public key from x509 cert"
|
||||
|
||||
pubKeyToJwk :: X509.PubKey -> Either Text JWK
|
||||
pubKeyToJwk pubKey = do
|
||||
@ -104,6 +112,11 @@ pubKeyToJwk pubKey = do
|
||||
return $ fromKeyMaterial $ RSAKeyMaterial (rsaKeyParams n e)
|
||||
X509.PubKeyEd25519 pubKeyEd ->
|
||||
return $ fromKeyMaterial $ OKPKeyMaterial (Ed25519Key pubKeyEd Nothing)
|
||||
X509.PubKeyEC pubKeyEc ->
|
||||
case ecParametersFromX509 pubKeyEc of
|
||||
Nothing -> Left "Error getting EC parameters from the public key"
|
||||
Just ecKeyParameters ->
|
||||
return $ fromKeyMaterial $ ECKeyMaterial ecKeyParameters
|
||||
_ -> Left "This key type is not supported"
|
||||
rsaKeyParams n e =
|
||||
RSAKeyParameters (Base64Integer n) (Base64Integer e) Nothing
|
||||
|
@ -642,6 +642,8 @@ def jwt_configuration(
|
||||
configuration = fixtures.jwt.init_rsa(tmp_path, configuration)
|
||||
case 'ed25519':
|
||||
configuration = fixtures.jwt.init_ed25519(tmp_path, configuration)
|
||||
case 'es':
|
||||
configuration = fixtures.jwt.init_es256(tmp_path, configuration)
|
||||
case _:
|
||||
raise Exception(f'Unsupported JWT configuration: {marker.args!r}')
|
||||
|
||||
|
@ -58,3 +58,26 @@ def init_ed25519(tmp_path: pathlib.Path, configuration: Any) -> JWTConfiguration
|
||||
algorithm = 'EdDSA',
|
||||
server_configuration = server_configuration,
|
||||
)
|
||||
|
||||
def init_es256(tmp_path: pathlib.Path, configuration: Any) -> JWTConfiguration:
|
||||
private_key_file = tmp_path / 'private.key'
|
||||
public_key_file = tmp_path / 'public.key'
|
||||
subprocess.run(['openssl', 'ecparam', '-name', 'prime256v1', '-genkey', '-noout', '-out', private_key_file], check=True, capture_output=True)
|
||||
subprocess.run(['openssl', 'ec', '-in', private_key_file, '-pubout', '-out', public_key_file], check=True, capture_output=True)
|
||||
with open(private_key_file) as f:
|
||||
private_key = f.read()
|
||||
with open(public_key_file) as f:
|
||||
public_key = f.read()
|
||||
server_configuration = {
|
||||
'type': 'ES256',
|
||||
'key': public_key,
|
||||
**configuration,
|
||||
}
|
||||
return JWTConfiguration(
|
||||
private_key_file = private_key_file,
|
||||
public_key_file = public_key_file,
|
||||
private_key = private_key,
|
||||
public_key = public_key,
|
||||
algorithm = 'ES256',
|
||||
server_configuration = server_configuration,
|
||||
)
|
||||
|
@ -350,6 +350,11 @@ class TestJwtBasicWithEd25519(AbstractTestJwtBasic):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.jwt('es')
|
||||
class TestJwtBasicWithEs(AbstractTestJwtBasic):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.jwt('rsa', {
|
||||
'header': {'type': 'Cookie', 'name': 'hasura_user'},
|
||||
})
|
||||
@ -364,6 +369,13 @@ class TestJwtBasicWithEd25519AndCookie(AbstractTestJwtBasic):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.jwt('es', {
|
||||
'header': {'type': 'Cookie', 'name': 'hasura_user'},
|
||||
})
|
||||
class TestJwtBasicWithEsAndCookie(AbstractTestJwtBasic):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.jwt('rsa', {
|
||||
'claims_format': 'stringified_json',
|
||||
})
|
||||
@ -378,6 +390,13 @@ class TestJwtBasicWithEd25519AndStringifiedJsonClaims(AbstractTestJwtBasic):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.jwt('es', {
|
||||
'claims_format': 'stringified_json',
|
||||
})
|
||||
class TestJwtBasicWithEsAndStringifiedJsonClaims(AbstractTestJwtBasic):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.jwt('rsa', {
|
||||
'claims_namespace_path': '$',
|
||||
})
|
||||
@ -392,6 +411,13 @@ class TestJwtBasicWithEd25519AndClaimsNamespacePathAtRoot(AbstractTestJwtBasic):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.jwt('es', {
|
||||
'claims_namespace_path': '$',
|
||||
})
|
||||
class TestJwtBasicWithEsAndClaimsNamespacePathAtRoot(AbstractTestJwtBasic):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.jwt('rsa', {
|
||||
'claims_namespace_path': '$.hasura_claims',
|
||||
})
|
||||
@ -406,6 +432,13 @@ class TestJwtBasicWithEd25519AndClaimsNamespacePathAtOneLevelOfNesting(AbstractT
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.jwt('es', {
|
||||
'claims_namespace_path': '$.hasura_claims',
|
||||
})
|
||||
class TestJwtBasicWithEsAndClaimsNamespacePathAtOneLevelOfNesting(AbstractTestJwtBasic):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.jwt('rsa', {
|
||||
'claims_namespace_path': '$.hasura.claims',
|
||||
})
|
||||
@ -420,6 +453,13 @@ class TestJwtBasicWithEd25519AndClaimsNamespacePathAtTwoLevelsOfNesting(Abstract
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.jwt('es', {
|
||||
'claims_namespace_path': '$.hasura.claims',
|
||||
})
|
||||
class TestJwtBasicWithEsAndClaimsNamespacePathAtTwoLevelsOfNesting(AbstractTestJwtBasic):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.jwt('rsa', {
|
||||
'claims_namespace_path': '$.hasura[\'claims%\']',
|
||||
})
|
||||
@ -434,6 +474,13 @@ class TestJwtBasicWithEd25519AndClaimsNamespacePathWithSpecialCharacters(Abstrac
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.jwt('es', {
|
||||
'claims_namespace_path': '$.hasura[\'claims%\']',
|
||||
})
|
||||
class TestJwtBasicWithEsAndClaimsNamespacePathWithSpecialCharacters(AbstractTestJwtBasic):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.admin_secret
|
||||
@pytest.mark.parametrize('endpoint', ['/v1/graphql', '/v1alpha1/graphql'])
|
||||
class AbstractTestJwtExpirySkew:
|
||||
@ -503,6 +550,13 @@ class TestJwtExpirySkewWithEd25519(AbstractTestJwtExpirySkew):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.jwt('es', {
|
||||
'allowed_skew': 60,
|
||||
})
|
||||
class TestJwtExpirySkewWithEs(AbstractTestJwtExpirySkew):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.admin_secret
|
||||
class AbstractTestSubscriptionJwtExpiry:
|
||||
def test_jwt_expiry(self, hge_key, jwt_configuration, ws_client):
|
||||
@ -540,6 +594,11 @@ class TestSubscriptionJwtExpiryWithEd25519(AbstractTestSubscriptionJwtExpiry):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.jwt('es')
|
||||
class TestSubscriptionJwtExpiryWithEs(AbstractTestSubscriptionJwtExpiry):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.admin_secret
|
||||
@pytest.mark.parametrize('endpoint', ['/v1/graphql', '/v1alpha1/graphql'])
|
||||
class AbstractTestJwtAudienceCheck:
|
||||
@ -620,6 +679,13 @@ class TestJwtAudienceCheckWithEd25519AndSingleAudience(AbstractTestJwtAudienceCh
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.jwt('es', {
|
||||
'audience': 'myapp-1234',
|
||||
})
|
||||
class TestJwtAudienceCheckWithEsAndSingleAudience(AbstractTestJwtAudienceCheck):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.jwt('rsa', {
|
||||
'audience': ['myapp-1234', 'myapp-9876'],
|
||||
})
|
||||
@ -634,6 +700,13 @@ class TestJwtAudienceCheckWithEd25519AndListOfAudiences(AbstractTestJwtAudienceC
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.jwt('es', {
|
||||
'audience': ['myapp-1234', 'myapp-9876'],
|
||||
})
|
||||
class TestJwtAudienceCheckWithEsAndListOfAudiences(AbstractTestJwtAudienceCheck):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.admin_secret
|
||||
@pytest.mark.parametrize('endpoint', ['/v1/graphql', '/v1alpha1/graphql'])
|
||||
class AbstractTestJwtIssuerCheck:
|
||||
@ -712,3 +785,10 @@ class TestJwtIssuerCheckWithRsa(AbstractTestJwtIssuerCheck):
|
||||
})
|
||||
class TestJwtIssuerCheckWithEd25519(AbstractTestJwtIssuerCheck):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.jwt('es', {
|
||||
'issuer': 'https://hasura.com',
|
||||
})
|
||||
class TestJwtIssuerCheckWithEs(AbstractTestJwtIssuerCheck):
|
||||
pass
|
||||
|
Loading…
Reference in New Issue
Block a user