diff --git a/api/src/main/java/com/stormpath/sdk/lang/DefaultRuntimeEnvironment.java b/api/src/main/java/com/stormpath/sdk/lang/DefaultRuntimeEnvironment.java new file mode 100644 index 0000000000..7ec25a243e --- /dev/null +++ b/api/src/main/java/com/stormpath/sdk/lang/DefaultRuntimeEnvironment.java @@ -0,0 +1,80 @@ +/* + * Copyright 2017 Stormpath, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.stormpath.sdk.lang; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.security.Provider; +import java.security.Security; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * @since 1.3.0 + */ +public final class DefaultRuntimeEnvironment implements RuntimeEnvironment { + + private static final Logger log = LoggerFactory.getLogger(DefaultRuntimeEnvironment.class); + + public static final DefaultRuntimeEnvironment INSTANCE = new DefaultRuntimeEnvironment(); + + private DefaultRuntimeEnvironment() { + } + + private static final String BC_PROVIDER_CLASS_NAME = "org.bouncycastle.jce.provider.BouncyCastleProvider"; + + private static final AtomicBoolean bcLoaded = new AtomicBoolean(false); + + private static void enableBouncyCastleIfPossible() { + + if (bcLoaded.get()) { + return; + } + + try { + Class clazz = Classes.forName(BC_PROVIDER_CLASS_NAME); + + //check to see if the user has already registered the BC provider: + + Provider[] providers = Security.getProviders(); + + for (Provider provider : providers) { + if (clazz.isInstance(provider)) { + bcLoaded.set(true); + return; + } + } + + //bc provider not enabled - add it: + Security.addProvider((Provider) Classes.newInstance(clazz)); + bcLoaded.set(true); + + } catch (UnknownClassException e) { + log.debug("Unable to load BouncyCastle. This is an acceptable outcome and this exception " + + "does not necessarily reflect a problem and can be ignored.", e); + //not available + } + } + + static { + enableBouncyCastleIfPossible(); + } + + @Override + public boolean isClassAvailable(String fqcn) { + return Classes.isAvailable(fqcn); + } +} diff --git a/api/src/main/java/com/stormpath/sdk/lang/RuntimeEnvironment.java b/api/src/main/java/com/stormpath/sdk/lang/RuntimeEnvironment.java new file mode 100644 index 0000000000..4621de0f18 --- /dev/null +++ b/api/src/main/java/com/stormpath/sdk/lang/RuntimeEnvironment.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017 Stormpath, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.stormpath.sdk.lang; + +/** + * @since 1.3.0 + */ +public interface RuntimeEnvironment { + + boolean isClassAvailable(String fqcn); + +} diff --git a/docs/source/forwarded-request.rst b/docs/source/forwarded-request.rst index 108b8810c3..f9d9ca6fae 100644 --- a/docs/source/forwarded-request.rst +++ b/docs/source/forwarded-request.rst @@ -687,7 +687,7 @@ security contexts. For example: ``notBeforeSeconds`` ^^^^^^^^^^^^^^^^^^^^ -You can specify a minimum timestamp of when the JWT is allowed to be used by the origin servers by +You can specify the earliest timestamp of when the JWT is allowed to be used by the origin servers by specifying the ``stormpath.zuul.account.header.jwt.notBeforeSeconds`` property. The value is a ``long`` that indicates the number of **seconds** (*not* milliseconds!) that will be added @@ -812,11 +812,10 @@ You may configure the signing key used to cryptographically sign the JWT via var However, it is probably unlikely that your backend origin servers will have this same key configured, so they will not be able to verify the JWT's digital signature. - To avoid JWT key/parsing errors in your origin servers, we recommend that specify your own signing key via - the :ref:`stormpath.zuul.account.header.jwt.key.value property ` or by - defining the :ref:`stormpathForwardedAccountJwtSigningKey ` bean. - - Also please see the :ref:`signing key alg ` section for more information. + To avoid JWT key/parsing errors in your origin servers, we recommend that specify your own signing key via either + the :ref:`stormpath.zuul.account.header.jwt.key.value ` or + :ref:`stormpath.zuul.account.header.jwt.key.resource ` properties or by + defining your own :ref:`stormpathForwardedAccountJwtSigningKey ` bean. .. _forwarded account signing key alg: @@ -858,17 +857,15 @@ For example: alg: HS256 -If you are using an HMAC algorithm by specifying ``HS256``, ``HS384``, or ``HS512``, you can provide your HMAC -symmetric key in one of two ways. Either: +You can provide the corresponding secret/private key in one of three ways: A. Set the ``stormpath.zuul.account.header.jwt.key.value`` and ``stormpath.zuul.account.header.jwt.key.encoding`` config properties, or -B. Define the :ref:`stormpathForwardedAccountJwtSigningKey ` bean. - +B. (for RSA and Elliptic Curve private keys only): Set the ``stormpath.zuul.account.header.jwt.key.resource`` to a + `Spring Resource Path `_, or -**If you are not using an HMAC algorithm**, you **must** provide your signing key -by defining the :ref:`stormpathForwardedAccountJwtSigningKey ` bean. +C. Define the :ref:`stormpathForwardedAccountJwtSigningKey ` bean. ``enabled`` @@ -893,13 +890,14 @@ This will ensure that the JWT created is *NOT* digitally signed - it will be an However, unsecured JWTs could be useful in very specific circumstances specific to your application. If you're unsure, we recommend that you *do not* set this property. +.. _forwarded account signing key encoding: ``encoding`` """""""""""" -If you specified the text value of your HMAC signing key via the ``stormpath.zuul.account.header.jwt.key.value`` property, -and that string is *not* Base64Url-encoded, you will need to set the ``stormpath.zuul.account.header.jwt.key.encoding`` -property to indicate which encoding is used. For example: +If you specified the text value of your private key via the ``stormpath.zuul.account.header.jwt.key.value`` property, +and that string is *not* Base64Url-encoded or PEM-encoded, you must set the +``stormpath.zuul.account.header.jwt.key.encoding`` property to indicate which encoding is used. For example: .. code-block:: yaml :emphasize-lines: 8 @@ -914,21 +912,31 @@ property to indicate which encoding is used. For example: encoding: base64 -The default/assumed encoding is ``base64url``. There are two other supported encodings: +There are four supported encodings: +* ``base64url``: standard Base64Url encoding, assumed by default * ``base64``: standard Base64 encoding (not URL encoded) * ``utf8``: direct UTF-8 bytes of the configured string, i.e. ``value.getBytes(StandardCharsets.UTF8)`` +* ``pem``: a `PEM-formatted `_ string, automatically detected + +The default/assumed encoding is ``base64url``, but PEM-encoded strings will be detected automatically. Therefore you +only need to set the ``stormpath.zuul.account.header.jwt.key.encoding`` property if your key value is ``base64`` or +``utf8`` encoded. -**CAUTION**: these 3 text encodings are not cryptographically secure. Please see the +**CAUTION**: these 4 text encodings are not inherently cryptographically secure. Please see the :ref:`key caution ` concerning key string values. -.. _forwarded account signing key value: -``value`` -""""""""" +``id`` +"""""" -If you want to configure your HMAC signing key as a string, you can set the -``stormpath.zuul.account.header.jwt.key.value`` property. For example: +When specifying a signing key, it is usually recommended to also specify a string identifier for the key in the JWT +header. This allows JWT recipients (i.e. your origin servers) the ability to inspect the JWT header and identify which +signing key was used. Based on this identifier, the JWT recipient can then look up the corresponding key +(or public key) to use in order to correctly verify the JWT's digital signature. + +You can specify your signing key's id (the ``id`` param in the JWT header) by setting the +``stormpath.zuul.account.header.jwt.key.id`` configuration property. For example: .. code-block:: yaml @@ -938,53 +946,37 @@ If you want to configure your HMAC signing key as a string, you can set the header: jwt: key: - value: EQDGRjSpZB87_eWO42XQ7h7mfxk0EmF6ZDY0TDGdAoA - - -By default, the key value is expected to be a Base64Url string. The |project| will then base64url-decode this value -at startup to obtain the raw signing key bytes used to compute the JWT signature. + id: my signing key id -If your value string is not Base64Url, you can specify the ``stormpath.zuul.account.header.jwt.key.encoding`` -config property to indicate which encoding is used. -.. _forwarded account signing key value caution: - -.. caution:: +This will set the JWT's ``kid`` header accordingly. - **Base64, Base64Url and UTF-8 encoding DOES NOT imply encryption**. +Note that since it is a header, an alternative approach of accomplishing the same thing is to set it as a +``stormpath.zuul.account.header.jwt.header.kid`` name/value pair: - Anyone that can access the - ``stormpath.zuul.account.header.jwt.key.value`` string value can use it to sign JWTs as you. Keep this text string (and - the configured property value) safe and secret. +.. code-block:: yaml - If you are uncomfortable embedding key strings in your configuration due to security concerns, we recommend - any of three approaches: + stormpath: + zuul: + account: + header: + jwt: + header: + kid: my signing key id - 1. Specify the ``stormpath.zuul.account.header.jwt.key.value`` value as an - `external Spring Boot property `_. - For example, set the ``STORMPATH_ZUUL_ACCOUNT_HEADER_JWT_KEY_K`` environment variable via an operations - orchestration mechanism like Chef, Puppet or CloudFoundry that has access to secure/encrypted data store for - such values. - 2. Use `Spring Cloud Config Server `_ - to securely represent key values as text properties in your config. Spring Cloud Config Server will decrypt - the text value just before giving it to the |project| so it may be used correctly. +The first approach keeps the key id configuration 'close to' the other key parameters, which might be desirable +depending on preference. Either approach accomplishes the same thing - feel free to use what you prefer. - 3. Do not configure the ``stormpath.zuul.account.header.jwt.key.value`` property and instead define your own - :ref:`stormpathForwardedAccountJwtSigningKey ` bean. You can then load the - key bytes in whatever secure way you prefer. +.. _forwarded account signing key resource: -``id`` -"""""" +``resource`` +"""""""""""" -When specifying a signing key, it is usually recommended to also specify a string identifier for the key in the JWT -header. This allows JWT recipients (i.e. your origin servers) the ability to inspect the JWT header and identify which -signing key was used. Based on this identifier, the JWT recipient can then look up the corresponding key -(or public key) to use in order to correctly verify the JWT's digital signature. - -You can specify your signing key's id (the ``id`` param in the JWT header) by setting the -``stormpath.zuul.account.header.jwt.key.id`` configuration property. For example: +If you want to use an RSA or Elliptic Curve private key to sign the JWT, you can point to a private key resource +(like a file or URL) instead of pasting the the key contents into your Spring configuration files by specifying the +``stormpath.zuul.account.header.jwt.key.resource`` property. For example: .. code-block:: yaml @@ -994,13 +986,47 @@ You can specify your signing key's id (the ``id`` param in the JWT header) by se header: jwt: key: - id: my signing key id + resource: classpath://myRsaPrivateKey.pem -This will set the JWT's ``kid`` header accordingly. +The ``resource`` property value can be any String path supported by the +`Spring ResourceLoader `_. -Note that since it is a header, an alternative approach of accomplishing the same thing is to set it as a -``stormpath.zuul.account.header.jwt.header.kid`` name/value pair: +The resource loaded must be a PEM-encoded RSA or Elliptic Curve PRIVATE KEY (public keys cannot be used to create +cryptographic signatures). However, **NOTE**: + +.. note:: + + Because PEM-encoding is not natively supported by the JVM, you must have BouncyCastle's ``bcpkix-jdk15on`` + artifact, version 1.56 or newer as a dependency in your application's runtime classpath. For example: + + **Maven**: + + .. code-block:: xml + + + org.bouncycastle + bcpkix-jdk15on + 1.56 + runtime + + + **Gradle**: + + .. code-block:: groovy + + dependencies { + runtime 'org.bouncycastle:bcpkix-jdk15on:1.56' + } + + +.. _forwarded account signing key value: + +``value`` +""""""""" + +If you want to configure your private signing key as a string, you can set the +``stormpath.zuul.account.header.jwt.key.value`` property. For example: .. code-block:: yaml @@ -1009,12 +1035,48 @@ Note that since it is a header, an alternative approach of accomplishing the sam account: header: jwt: - header: - kid: my signing key id + key: + value: EQDGRjSpZB87_eWO42XQ7h7mfxk0EmF6ZDY0TDGdAoA -The first approach keeps the key id configuration 'close to' the other key parameters, which might be desirable -depending on preference. Either approach accomplishes the same thing - feel free to use what you prefer. +By default, the key value is expected to be a Base64Url string, but PEM-encoded values will be detected automatically. +The |project| will then base64url-decode (or PEM-decode) this value at startup to obtain the private signing key bytes +used to compute the JWT signature. + +If your value string is not Base64Url or PEM-encoded, you must also specify the +:ref:`stormpath.zuul.account.header.jwt.key.encoding ` property. + +.. _forwarded account signing key value caution: + +.. caution:: + + **Base64, Base64Url, UTF-8 and PEM encoding DOES NOT imply encryption**. + + Anyone that can access the + ``stormpath.zuul.account.header.jwt.key.value`` string value can use it to sign JWTs as you. Keep this text string + (and the configured property value) safe and secret. + + If you are uncomfortable embedding key strings in your configuration due to security concerns, we recommend + any of four approaches: + + 1. Specify the ``stormpath.zuul.account.header.jwt.key.value`` value as an + `external Spring Boot property `_. + For example, set the ``STORMPATH_ZUUL_ACCOUNT_HEADER_JWT_KEY_K`` environment variable via an operations + orchestration mechanism like Chef, Puppet or CloudFoundry that has access to secure/encrypted data store for + such values. + + 2. Use `Spring Cloud Config Server `_ + to securely represent key values as text properties in your config. Spring Cloud Config Server will decrypt + the text value just before giving it to the |project| so it may be used correctly. + + 3. If using an RSA or Elliptic Curve private key, configure the + :ref:`stormpath.zuul.account.header.jwt.key.resource ` + property instead. This will load your private key from the specified resource location, which can reflect a + secured file system or URL for example. + + 4. Do not configure the ``stormpath.zuul.account.header.jwt.key.value`` property and instead define your own + :ref:`stormpathForwardedAccountJwtSigningKey ` bean. You can then load the + key bytes in whatever secure way you prefer. ``valueClaim`` @@ -1154,20 +1216,17 @@ This would result in JWT claims that look something like this: Signing Key Bean """""""""""""""" -If you are using an RSA or Elliptic Curve private key to sign the JWT, or you just prefer to specify your signing key -as a bean, you must provide the key by defining a ``stormpathForwardedAccountJwtSigningKey`` bean in your Spring -configuration: +If you cannot or do not want to load your secret/private key using the configuration properties above, you can provide +the key by defining a ``stormpathForwardedAccountJwtSigningKey`` bean in your Spring configuration: .. code-block:: java @Bean public java.security.Key stormpathForwardedAccountJwtSigningKey() { - //load the RSA or Elliptic Curve private key here and return it. + //load the HMAC, RSA or Elliptic Curve private key here and return it. } - -You can also define this bean to provide your symmetric key for HMAC algorithms as well if you prefer not to -configure the HMAC signing key using the ``stormpath.zuul.account.header.jwt.key.value`` config property. +This allows you finer grained control to look up the Key from whatever source you prefer. Custom Header Value diff --git a/examples/zuul-spring-cloud-starter/src/main/resources/README.md b/examples/zuul-spring-cloud-starter/src/main/resources/README.md new file mode 100644 index 0000000000..d1cb55df1e --- /dev/null +++ b/examples/zuul-spring-cloud-starter/src/main/resources/README.md @@ -0,0 +1,27 @@ +The keys in this directory are for testing purposes only, never use them in a real application. + +## RSA Test Keys + +The `rsatest.priv.pem` RSA private key file was generated via the following: + + $ openssl genrsa -out rsatest.priv.pem 2048 + +That private key's corresponding `rsatest.pub.pem` public key was derived via: + + $ openssl rsa -in rsatest.priv.pem -pubout > rsatest.pub.pem + + +## Elliptic Curve Test Keys + +The `secp384r1.priv.pem` Elliptic Curve private key file was generated via: + + $ openssl ecparam -name secp384r1 -genkey -noout -out secp384r1.priv.pem + +Note that this explicitly references the EC curve name `secp384r1`. + +For JWT's `ES256`, `ES384` and `ES512` signature algorithms, the +respective OpenSSL curve names are `secp256k1`, `secp384r1` and `secp512r1`. + +That private key's corresponding `secp384r1.pub.pem` public key was derived via: + + $ openssl ec -in secp384r1.priv.pem -pubout -out secp384r1.pub.pem \ No newline at end of file diff --git a/examples/zuul-spring-cloud-starter/src/main/resources/application.yml b/examples/zuul-spring-cloud-starter/src/main/resources/application.yml index 66a93f1ab9..fe6deb350b 100644 --- a/examples/zuul-spring-cloud-starter/src/main/resources/application.yml +++ b/examples/zuul-spring-cloud-starter/src/main/resources/application.yml @@ -10,4 +10,16 @@ server: logging: level: - root: INFO \ No newline at end of file + root: INFO + +stormpath: + zuul: + account: + header: + jwt: + key: + resource: classpath:rsatest.priv.pem + # this is just one example of a key id - anything that the origin server can make sense of to lookup + # the corresponding public key is fine. Here we use the public key file name (which the example origin app + # has access to) + id: rsatest.pub.pem diff --git a/examples/zuul-spring-cloud-starter/src/main/resources/rsatest.priv.pem b/examples/zuul-spring-cloud-starter/src/main/resources/rsatest.priv.pem new file mode 100644 index 0000000000..37be78c8cc --- /dev/null +++ b/examples/zuul-spring-cloud-starter/src/main/resources/rsatest.priv.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA9K1RO7ABH1CdCDflO/V2JesfKGRDdeuyJQe4OqkTHR1LOcLP +KrCrnu+zYf1cLxemwgcbRY3RQAwJsMXNVT90kA2zfa4BgtGucckS1yTDTrrgQhFs +t16fw+fNQLvuGHRY5xshV9wrFXDsX71GHKXoA2QhBPtSGw1yiYfPtDwOvQ9jxBQk +xVJ6YsCk9nqoU5oSIsA0rRC995RAoNqo1DAjX5OhPqaOg2+3y6fV8fGI+xprEmv1 +owFiq/NFY5PtyOn8W0jSsIKLDPSogzYfeAR+Ryw0iSl5rHmLeEn16oy1aeKsApg1 +CkGTXOY7VyH0PsziTuLlOufZ1HyuGPUv7hQeUwIDAQABAoIBAQCJEou+v4Rxcaz3 +jLDcnU/6QDVtYHY2mrtraV65ZjzyA5ZAHrYGuYD8AldhXxoEu+BNNMP/fEqs8dF/ ++eBlkK4Rgct7bj8kdamfz0DBzLOp6KF4AeEA/X7Nto/TYzUo+A1SM23DlfGBCokx +vYyIwh0vwSmKa+18gFUZXT9sPnUXTm5jrfrXpRDyHyk7Kc7+2MAUkypoR9b9Qkmv +JVmDT6UJtWqOxAx+xanK475IZoz6rC5WLffQ+oDrOToJO6FMnB6jh1vuPpkaMZ4r +vqmhSAMIqFy3F4gf3IYUCLehz68NSvrcdz136tIxfIdBUtB6eONpcO8RtbqtJygX +xENgPXfBAoGBAP7GU73H3piA1U+QeszxfOGmxnPQ6SrwRsyc+kPd90fi59pRQeFS +dXOZfDNiWOUREx4QUB9WGnuvNqmOHV975psr+y1sgZW0+azIwgvhyGOdGZ+jxU6W +fDRJbpDiMpP6ywWQtQn4mPXZUreFL1m9Ix1kCySoHS30NsTpgsx9rMihAoGBAPXa +juLTlvNXNIB645+qRL7ggx8Hd4Gza3+mQT1U7iEZZ9AhQic6PHQfONqZUwCEHZB5 +DXsCFyWIm0x2EqtVSzPy5kJcl67oW4mnVCOm9SfNlqBrLGD5frRBufME/vxBjc6n +JMKcZ6ocgkCjzy8ZIIHC/eckOJ67xcXwclh5fz5zAoGAYcY9FwUgYQh4VHuPFR3M +HlFBserHwQnLMfVAelEx+C2VawxqKw3ZM08BAjtJAEfoPU5nYU9LBJJ+eN2oWh+T +pZNgZtNQe+KjOvMkvSieHdSJo+FW9Ez+R5ayzvlwDahex7j8MWJtWVRY0UNUo6zZ +UAs3146I/DzP1AwFfXLxn2ECgYBsYYQZ9IMYFTp85S/RZENYDitfk3AYilr6c/VQ +r08m4kdEllTObDrYSidLHspbcOKDnQnXT02a60TjCS4jv78eUJc3bBAmOCKaZVyP +NvveJyCe6YAv4+z6U/tAadRqqg90qXRoIoEEmfrFujEMpzwQWECMFAit2UNPhjcy +T6VLhwKBgQD7xpcvkDjx5UKzB5/yybcPSUipPaQl8gAPLV3kjDluF49eDPMu+afb +GLpheRkIpWfrCesYoBoJdb/CngiJ4sDaMncQRnGSmGjrZU9lBGN7UvaEDbJhZvej +S5RJw6iMo1PLd+ikOaFTpbsFt89l8x00A7fQu1fqmvg6CQd+gIQDtg== +-----END RSA PRIVATE KEY----- diff --git a/examples/zuul-spring-cloud-starter/src/main/resources/rsatest.pub.pem b/examples/zuul-spring-cloud-starter/src/main/resources/rsatest.pub.pem new file mode 100644 index 0000000000..84cd3fb94d --- /dev/null +++ b/examples/zuul-spring-cloud-starter/src/main/resources/rsatest.pub.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9K1RO7ABH1CdCDflO/V2 +JesfKGRDdeuyJQe4OqkTHR1LOcLPKrCrnu+zYf1cLxemwgcbRY3RQAwJsMXNVT90 +kA2zfa4BgtGucckS1yTDTrrgQhFst16fw+fNQLvuGHRY5xshV9wrFXDsX71GHKXo +A2QhBPtSGw1yiYfPtDwOvQ9jxBQkxVJ6YsCk9nqoU5oSIsA0rRC995RAoNqo1DAj +X5OhPqaOg2+3y6fV8fGI+xprEmv1owFiq/NFY5PtyOn8W0jSsIKLDPSogzYfeAR+ +Ryw0iSl5rHmLeEn16oy1aeKsApg1CkGTXOY7VyH0PsziTuLlOufZ1HyuGPUv7hQe +UwIDAQAB +-----END PUBLIC KEY----- diff --git a/examples/zuul-spring-cloud-starter/src/main/resources/secp384r1.priv.pem b/examples/zuul-spring-cloud-starter/src/main/resources/secp384r1.priv.pem new file mode 100644 index 0000000000..0c1f8227fb --- /dev/null +++ b/examples/zuul-spring-cloud-starter/src/main/resources/secp384r1.priv.pem @@ -0,0 +1,6 @@ +-----BEGIN EC PRIVATE KEY----- +MIGjAgEBBC92z4hOc4k1OWWlVHhNXqpXgeZvajIPAy1Y4HSgbi2dCVnl2fnePUyT +HSAkhXqXEqAHBgUrgQQAIqFkA2IABJqEhTSo3jWGw0NQibaAWW91i8UuVaLEYt2t +MWsLZKIu9za4sGBeQBZv5TgnrbN6mKrC6ro0MYb2m40D9mezqDJYqgeGDHzT/ogA +x95LcAMfWg5WjuUqJ1aWYVZJgHULag== +-----END EC PRIVATE KEY----- diff --git a/examples/zuul-spring-cloud-starter/src/main/resources/secp384r1.pub.pem b/examples/zuul-spring-cloud-starter/src/main/resources/secp384r1.pub.pem new file mode 100644 index 0000000000..fe75022972 --- /dev/null +++ b/examples/zuul-spring-cloud-starter/src/main/resources/secp384r1.pub.pem @@ -0,0 +1,5 @@ +-----BEGIN PUBLIC KEY----- +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEmoSFNKjeNYbDQ1CJtoBZb3WLxS5VosRi +3a0xawtkoi73NriwYF5AFm/lOCets3qYqsLqujQxhvabjQP2Z7OoMliqB4YMfNP+ +iADH3ktwAx9aDlaO5SonVpZhVkmAdQtq +-----END PUBLIC KEY----- diff --git a/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/pom.xml b/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/pom.xml index 6387a40f03..c5e5a854aa 100644 --- a/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/pom.xml +++ b/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/pom.xml @@ -40,5 +40,11 @@ com.stormpath.sdk stormpath-sdk-zuul + + org.bouncycastle + bcpkix-jdk15on + ${bouncycastle.version} + true + \ No newline at end of file diff --git a/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/main/java/com/stormpath/spring/cloud/zuul/autoconfigure/ClientApplicationJwkFactory.java b/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/main/java/com/stormpath/spring/cloud/zuul/autoconfigure/ClientApplicationJwkFactory.java new file mode 100644 index 0000000000..5cfd815892 --- /dev/null +++ b/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/main/java/com/stormpath/spring/cloud/zuul/autoconfigure/ClientApplicationJwkFactory.java @@ -0,0 +1,90 @@ +/* + * Copyright 2017 Stormpath, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.stormpath.spring.cloud.zuul.autoconfigure; + +import com.stormpath.sdk.api.ApiKey; +import com.stormpath.sdk.application.Application; +import com.stormpath.sdk.client.Client; +import com.stormpath.sdk.lang.Assert; +import com.stormpath.sdk.lang.Function; +import com.stormpath.spring.cloud.zuul.config.DefaultJwkResult; +import com.stormpath.spring.cloud.zuul.config.JwkResult; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.impl.TextCodec; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.crypto.spec.SecretKeySpec; +import java.security.Key; + +import static com.stormpath.spring.cloud.zuul.autoconfigure.ConfigJwkFactory.*; + +/** + * @since 1.3.0 + */ +public class ClientApplicationJwkFactory implements Function { + + private static final Logger log = LoggerFactory.getLogger(ClientApplicationJwkFactory.class); + + private final Client client; + + private final Application application; + + public ClientApplicationJwkFactory(Client client, Application application) { + Assert.notNull(client); + Assert.notNull(application); + this.client = client; + this.application = application; + } + + @Override + public JwkResult apply(SignatureAlgorithm signatureAlgorithm) { + + //fall back to client api key secret: + ApiKey apiKey = client.getApiKey(); + + //Set a 'kid' equal to the api key href: + String href = application.getHref(); + int i = href.indexOf("/applications/"); + href = href.substring(0, i); + href += "/apiKeys/" + apiKey.getId(); + String kid = href; + + String secret = apiKey.getSecret(); + byte[] bytes = TextCodec.BASE64.decode(secret); + + SignatureAlgorithm defaultSigAlg = getAlgorithm(bytes); + if (signatureAlgorithm == null) { + signatureAlgorithm = defaultSigAlg; + } + + if (!signatureAlgorithm.isHmac()) { + String msg = "Unable to use specified JWT signature algorithm '" + signatureAlgorithm + "' when " + + "creating X-Forwarded-User JWTs, as this algorithm is incompatible with the " + + "fallback/default Stormpath Client ApiKey secret signing key. Defaulting to '" + + defaultSigAlg + "'. To avoid this message, either 1) do not specify a signature algorithm to " + + "let the framework choose an algorithm appropriate for the default signing key, or 2) define " + + "a 'stormpathForwardedAccountJwtSigningKey' bean of type java.security.Key that is " + + "compatible with your specified signature algorithm."; + log.warn(msg); + signatureAlgorithm = defaultSigAlg; + } + + Key key = new SecretKeySpec(bytes, signatureAlgorithm.getJcaName()); + + return new DefaultJwkResult(signatureAlgorithm, key, kid); + } +} diff --git a/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/main/java/com/stormpath/spring/cloud/zuul/autoconfigure/ConfigJwkFactory.java b/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/main/java/com/stormpath/spring/cloud/zuul/autoconfigure/ConfigJwkFactory.java new file mode 100644 index 0000000000..ab38ef7a75 --- /dev/null +++ b/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/main/java/com/stormpath/spring/cloud/zuul/autoconfigure/ConfigJwkFactory.java @@ -0,0 +1,223 @@ +/* + * Copyright 2017 Stormpath, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.stormpath.spring.cloud.zuul.autoconfigure; + +import com.stormpath.sdk.lang.Assert; +import com.stormpath.sdk.lang.Function; +import com.stormpath.sdk.lang.RuntimeEnvironment; +import com.stormpath.sdk.lang.Strings; +import com.stormpath.spring.cloud.zuul.config.DefaultJwkResult; +import com.stormpath.spring.cloud.zuul.config.JwkConfig; +import com.stormpath.spring.cloud.zuul.config.JwkResult; +import com.stormpath.spring.cloud.zuul.config.PemResourceKeyResolver; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.SignatureException; +import io.jsonwebtoken.impl.TextCodec; +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.Resource; + +import javax.crypto.spec.SecretKeySpec; +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.security.PrivateKey; +import java.security.interfaces.ECKey; +import java.security.interfaces.RSAKey; + +/** + * @since 1.3.0 + */ +public class ConfigJwkFactory implements Function { + + private final RuntimeEnvironment runtimeEnvironment; + private final Function defaultKeyFunction; + + public ConfigJwkFactory(RuntimeEnvironment runtimeEnvironment, + Function defaultKeyFunction) { + Assert.notNull(runtimeEnvironment); + Assert.notNull(defaultKeyFunction); + this.runtimeEnvironment = runtimeEnvironment; + this.defaultKeyFunction = defaultKeyFunction; + } + + @Override + public JwkResult apply(JwkConfig jwk) { + + SignatureAlgorithm signatureAlgorithm = null; + String kid = jwk.getId(); + Key key = null; + + String value = jwk.getAlg(); + if (value != null) { + try { + signatureAlgorithm = SignatureAlgorithm.forName(value); + } catch (SignatureException e) { + String msg = "Unsupported stormpath.zuul.account.header.jwt.key.alg value: " + value + ". " + + "Please use only " + SignatureAlgorithm.class.getName() + " enum names: " + + Strings.arrayToCommaDelimitedString(SignatureAlgorithm.values()).replace("NONE,", ""); + throw new IllegalArgumentException(msg, e); + } + } + + byte[] bytes = null; + + Resource keyResource = jwk.getResource(); + + String keyString = jwk.getValue(); + + boolean keyStringSpecified = Strings.hasText(keyString); + + if (keyResource != null && keyStringSpecified) { + String msg = "Both the stormpath.zuul.account.header.jwt.key.value and " + + "stormpath.zuul.account.header.jwt.key.resource properties may not be set simultaneously. " + + "Please choose one."; + throw new IllegalArgumentException(msg); + } + + if (keyStringSpecified) { + + String encoding = jwk.getEncoding(); + + if (keyString.startsWith(PemResourceKeyResolver.PEM_PREFIX)) { + encoding = "pem"; + } + + if (encoding == null) { + //default to the JWK specification format: + encoding = "base64url"; + } + + if (encoding.equalsIgnoreCase("base64url")) { + bytes = TextCodec.BASE64URL.decode(keyString); + } else if (encoding.equalsIgnoreCase("base64")) { + bytes = TextCodec.BASE64.decode(keyString); + } else if (encoding.equalsIgnoreCase("utf8")) { + bytes = keyString.getBytes(StandardCharsets.UTF_8); + } else if (encoding.equalsIgnoreCase("pem")) { + byte[] resourceBytes = keyString.getBytes(StandardCharsets.UTF_8); + ByteArrayInputStream bais = new ByteArrayInputStream(resourceBytes); + String description = "stormpath.zuul.account.header.jwt.key.value"; + keyResource = new InputStreamResource(bais, description); + } else { + throw new IllegalArgumentException("Unsupported encoding '" + encoding + "'. Supported " + + "encodings: base64url, base64, utf8, pem."); + } + } + + if (bytes != null && bytes.length > 0) { //symmetric key + + if (signatureAlgorithm == null) { + //choose the best available alg based on available key: + signatureAlgorithm = getAlgorithm(bytes); + } + + if (!signatureAlgorithm.isHmac()) { + String algName = signatureAlgorithm.name(); + String msg = "It appears that the stormpath.zuul.account.header.jwt.key.value " + + "is a shared (symmetric) secret key, and this requires the " + + "stormpath.zuul.account.header.jwt.key.alg value to equal HS256, HS384, or HS512. " + + "The specified stormpath.zuul.account.header.jwt.key.alg value is " + algName + ". " + + "If you wish to use the " + algName + " algorithm, please ensure that either 1) " + + "stormpath.zuul.account.header.jwt.key.value is a private asymmetric PEM-encoded string, " + + "or 2) set the stormpath.zuul.account.header.jwt.key.resource property to a Spring " + + "Resource path where the PEM-encoded key file resides, or " + + "or 3) define a bean named 'stormpathForwardedAccountJwtSigningKey' that returns an " + + signatureAlgorithm.getFamilyName() + " private key instance."; + throw new IllegalArgumentException(msg); + } + + key = new SecretKeySpec(bytes, signatureAlgorithm.getJcaName()); + } + + if (keyResource != null) { + + Function resourceKeyResolver = createResourceKeyFunction(keyResource, keyStringSpecified); + Assert.notNull(resourceKeyResolver, "resourceKeyResolver instance cannot be null."); + key = resourceKeyResolver.apply(keyResource); + if (key == null) { + String msg = "Resource to Key resolver/function did not return a key for specified resource [" + + keyResource + "]. If providing your own implementation of this function, ensure it does not " + + "return null."; + throw new IllegalStateException(msg); + } + + Assert.notNull(key, "ResourceKeyResolver function did not return a key for specified resource [" + keyResource + "]"); + + if (signatureAlgorithm == null) { + if (key instanceof RSAKey) { + signatureAlgorithm = SignatureAlgorithm.RS256; + } else if (key instanceof ECKey) { + signatureAlgorithm = SignatureAlgorithm.ES256; + } else { + String msg = "Unable to detect jwt signing key type to provide a default signature " + + "algorithm. Please specify the stormpath.zuul.account.header.jwt.key.alg property."; + throw new IllegalArgumentException(msg); + } + } + + if (key instanceof RSAKey && !signatureAlgorithm.getFamilyName().equalsIgnoreCase("RSA")) { + String msg = "Signature algorithm [" + signatureAlgorithm + "] is not " + + "compatible with the specified RSA key."; + throw new IllegalArgumentException(msg); + } + + if (key instanceof ECKey && !signatureAlgorithm.getFamilyName().equalsIgnoreCase("Elliptic Curve")) { + String msg = "Signature algorithm [" + signatureAlgorithm + "] is not " + + "compatible with the specified Elliptic Curve key."; + throw new IllegalArgumentException(msg); + } + + Assert.isTrue(key instanceof PrivateKey, "Specified asymmetric signing key is not a PrivateKey. " + + "Please ensure you specify a private (not public) key."); + } + + if (key == null) { + //a key was not provided as a bean, nor was one configured via app config properties. + //fall back to the default key as the signing key + return defaultKeyFunction.apply(signatureAlgorithm); + } + + return new DefaultJwkResult(signatureAlgorithm, key, kid); + } + + protected Function createResourceKeyFunction(Resource keyResource, boolean keyStringSpecified) { + + if (!runtimeEnvironment.isClassAvailable("org.bouncycastle.openssl.PEMParser")) { + + String msg = "The org.bouncycastle:bcpkix-jdk15on:1.56 artifact (or newer) must be in the " + + "classpath to be able to parse the " + + (keyStringSpecified ? + "stormpath.zuul.account.header.jwt.key.value PEM-encoded value" : + "stormpath.zuul.account.header.jwt.key.resource [" + keyResource + "]."); + throw new IllegalStateException(msg); + } + + return new PemResourceKeyResolver(true); //origin server == private key + } + + //package private on purpose + static SignatureAlgorithm getAlgorithm(byte[] hmacSigningKeyBytes) { + Assert.isTrue(hmacSigningKeyBytes != null && hmacSigningKeyBytes.length > 0, + "hmacSigningBytes cannot be null or empty."); + if (hmacSigningKeyBytes.length >= 64) { + return SignatureAlgorithm.HS512; + } else if (hmacSigningKeyBytes.length >= 48) { + return SignatureAlgorithm.HS384; + } else { //<= 32 + return SignatureAlgorithm.HS256; + } + } +} diff --git a/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/main/java/com/stormpath/spring/cloud/zuul/autoconfigure/StormpathZuulAutoConfiguration.java b/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/main/java/com/stormpath/spring/cloud/zuul/autoconfigure/StormpathZuulAutoConfiguration.java index ccb6a360f9..5bc9dc3fd0 100644 --- a/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/main/java/com/stormpath/spring/cloud/zuul/autoconfigure/StormpathZuulAutoConfiguration.java +++ b/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/main/java/com/stormpath/spring/cloud/zuul/autoconfigure/StormpathZuulAutoConfiguration.java @@ -18,7 +18,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.netflix.zuul.ZuulFilter; import com.stormpath.sdk.account.Account; -import com.stormpath.sdk.api.ApiKey; import com.stormpath.sdk.application.Application; import com.stormpath.sdk.client.Client; import com.stormpath.sdk.convert.Conversion; @@ -26,21 +25,22 @@ import com.stormpath.sdk.convert.ResourceConverter; import com.stormpath.sdk.lang.Assert; import com.stormpath.sdk.lang.Collections; +import com.stormpath.sdk.lang.DefaultRuntimeEnvironment; import com.stormpath.sdk.lang.Function; +import com.stormpath.sdk.lang.RuntimeEnvironment; +import com.stormpath.sdk.lang.Strings; import com.stormpath.sdk.servlet.account.AccountResolver; import com.stormpath.sdk.servlet.account.AccountStringResolver; import com.stormpath.sdk.servlet.http.Resolver; import com.stormpath.sdk.servlet.json.JsonFunction; import com.stormpath.spring.boot.autoconfigure.StormpathWebMvcAutoConfiguration; import com.stormpath.spring.cloud.zuul.config.JwkConfig; +import com.stormpath.spring.cloud.zuul.config.JwkResult; import com.stormpath.spring.cloud.zuul.config.JwtConfig; import com.stormpath.spring.cloud.zuul.config.StormpathZuulAccountHeaderConfig; import com.stormpath.spring.cloud.zuul.config.ValueClaimConfig; import com.stormpath.zuul.account.ForwardedAccountHeaderFilter; import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.impl.TextCodec; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.AutoConfigureAfter; @@ -52,8 +52,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import javax.crypto.spec.SecretKeySpec; -import java.nio.charset.StandardCharsets; import java.security.Key; import java.util.LinkedHashMap; import java.util.Map; @@ -68,8 +66,6 @@ @AutoConfigureAfter({ZuulProxyConfiguration.class, StormpathWebMvcAutoConfiguration.class}) public class StormpathZuulAutoConfiguration { - private static final Logger log = LoggerFactory.getLogger(StormpathZuulAutoConfiguration.class); - @SuppressWarnings("SpringJavaAutowiringInspection") @Autowired private AccountResolver accountResolver; //provided by StormpathWebMvcAutoConfiguration @@ -87,6 +83,9 @@ public class StormpathZuulAutoConfiguration { @Autowired private StormpathZuulAccountHeaderConfig accountHeader; + @Autowired(required = false) + private RuntimeEnvironment runtimeEnvironment = DefaultRuntimeEnvironment.INSTANCE; + @SuppressWarnings("SpringJavaAutowiringInspection") @Autowired private Client client; @@ -119,22 +118,6 @@ public Key stormpathForwardedAccountJwtSigningKey() { return null; } - /** - * @since 1.3.0 - */ - protected static SignatureAlgorithm getAlgorithm(byte[] hmacSigningKeyBytes) { - if (hmacSigningKeyBytes == null || hmacSigningKeyBytes.length == 0) { - return null; - } - if (hmacSigningKeyBytes.length >= 64) { - return SignatureAlgorithm.HS512; - } else if (hmacSigningKeyBytes.length >= 48) { - return SignatureAlgorithm.HS384; - } else { //<= 32 - return SignatureAlgorithm.HS256; - } - } - /** * @since 1.3.0 */ @@ -145,123 +128,34 @@ protected static SignatureAlgorithm getAlgorithm(byte[] hmacSigningKeyBytes) { final JwtConfig jwt = accountHeader.getJwt(); - JwkConfig jwk = jwt.getKey(); - - if (jwk == null) { - jwk = new JwkConfig(); - } + final JwkConfig jwk = jwt.getKey(); SignatureAlgorithm signatureAlgorithm = null; Key key = null; - boolean keyEnabled = jwk.isEnabled(); - - Map baseHeader = new LinkedHashMap<>(); - if (!Collections.isEmpty(jwt.getHeader())) { - baseHeader.putAll(jwt.getHeader()); - } - - if (keyEnabled) { - - String value = jwk.getAlg(); - if (value != null) { - signatureAlgorithm = SignatureAlgorithm.forName(value); - } - - String kid = jwk.getId(); - - key = stormpathForwardedAccountJwtSigningKey(); //check if explicitly provided as a bean + String kid = null; - if (key == null) { + if (jwk.isEnabled()) { - byte[] bytes = null; + Function defaultKeyFunction = + new ClientApplicationJwkFactory(this.client, this.application); - String encodedKeyBytes = jwk.getValue(); - if (encodedKeyBytes != null) { + Function keyFunction = new ConfigJwkFactory(runtimeEnvironment, defaultKeyFunction); - String encoding = jwk.getEncoding(); - if (encoding == null) { - //default to the JWK specification format: - encoding = "base64url"; - } - - if (encoding.equalsIgnoreCase("base64url")) { - bytes = TextCodec.BASE64URL.decode(encodedKeyBytes); - } else if (encoding.equalsIgnoreCase("base64")) { - bytes = TextCodec.BASE64.decode(encodedKeyBytes); - } else if (encoding.equalsIgnoreCase("utf8")) { - bytes = encodedKeyBytes.getBytes(StandardCharsets.UTF_8); - } else { - throw new IllegalArgumentException("Unsupported encoding '" + encoding + "'. Supported " + - "encodings: base64url, base64, utf8"); - } - } - - if (bytes != null && bytes.length > 0) { - - if (signatureAlgorithm == null) { - //choose the best available alg based on available key: - signatureAlgorithm = getAlgorithm(bytes); - } - - if (!signatureAlgorithm.isHmac()) { - String algName = signatureAlgorithm.name(); - String msg = "The stormpath.zuul.account.header.jwt.key.k property may only be specified " + - "when the stormpath.zuul.account.header.jwt.key.alg value equals HS256, HS384, or HS512. " + - "The specified stormpath.zuul.account.header.jwt.key.alg value is " + algName + ". " + - "When using " + algName + ", please please define a bean named " + - "'stormpathForwardedAccountJwtSigningKey' that returns an " + - signatureAlgorithm.getFamilyName() + " private key instance."; - throw new IllegalArgumentException(msg); - } - - key = new SecretKeySpec(bytes, signatureAlgorithm.getJcaName()); - } - } - - if (key == null) { - - //a key was not provided as a bean, nor was one configured via app config properties. - //fall back to the Stormpath Client API Key Secret as the signing key - - //fall back to client api key secret: - ApiKey apiKey = client.getApiKey(); - - //Set a 'kid' equal to the api key href: - String href = application.getHref(); - int i = href.indexOf("/applications/"); - href = href.substring(0, i); - href += "/apiKeys/" + apiKey.getId(); - kid = href; - - String secret = apiKey.getSecret(); - byte[] bytes = TextCodec.BASE64.decode(secret); - - SignatureAlgorithm defaultSigAlg = getAlgorithm(bytes); - if (signatureAlgorithm == null) { - signatureAlgorithm = defaultSigAlg; - } - if (!signatureAlgorithm.isHmac()) { - String msg = "Unable to use specified JWT signature algorithm '" + signatureAlgorithm + "' when " + - "creating X-Forwarded-User JWTs, as this algorithm is incompatible with the " + - "fallback/default Stormpath Client ApiKey secret signing key. Defaulting to '" + - defaultSigAlg + "'. To avoid this message, either 1) do not specify a signature algorithm to " + - "let the framework choose an algorithm appropriate for the default signing key, or 2) define " + - "a 'stormpathForwardedAccountJwtSigningKey' bean of type java.security.Key that is " + - "compatible with your specified signature algorithm."; - log.warn(msg); - signatureAlgorithm = defaultSigAlg; - } - - key = new SecretKeySpec(bytes, signatureAlgorithm.getJcaName()); - } + JwkResult jwkResult = keyFunction.apply(jwk); + key = jwkResult.getKey(); + kid = jwkResult.getKeyId(); + signatureAlgorithm = jwkResult.getSignatureAlgorithm(); + } - if (kid != null) { - baseHeader.put("kid", kid); - } + Map baseHeader = new LinkedHashMap<>(); + if (!Collections.isEmpty(jwt.getHeader())) { + baseHeader.putAll(jwt.getHeader()); + } + if (Strings.hasText(kid)) { + baseHeader.put("kid", kid); } String valueClaimName = null; - ValueClaimConfig valueClaim = jwt.getValueClaim(); if (valueClaim != null && valueClaim.isEnabled()) { valueClaimName = valueClaim.getName(); @@ -287,22 +181,23 @@ public Function stormpathForwardedAccountStringFunction() { final JwtConfig jwt = accountHeader.getJwt(); return new Function() { + @SuppressWarnings("unchecked") @Override public String apply(Account account) { Object value = accountFunction.apply(account); - if (value == null || (value instanceof Map && Collections.isEmpty((Map)value))) { + if (value == null || (value instanceof Map && Collections.isEmpty((Map) value))) { return null; } if (value instanceof String) { - return (String)value; + return (String) value; } if (jwt.isEnabled()) { Assert.isInstanceOf(Map.class, value, "stormpathForwardedAccountMapFunction must return a String or Map when using JWT."); - Map map = (Map)value; + @SuppressWarnings("ConstantConditions") Map map = (Map) value; return jwtFunction.apply(map); } diff --git a/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/main/java/com/stormpath/spring/cloud/zuul/config/DefaultJwkResult.java b/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/main/java/com/stormpath/spring/cloud/zuul/config/DefaultJwkResult.java new file mode 100644 index 0000000000..0f07923eaf --- /dev/null +++ b/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/main/java/com/stormpath/spring/cloud/zuul/config/DefaultJwkResult.java @@ -0,0 +1,54 @@ +/* + * Copyright 2017 Stormpath, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.stormpath.spring.cloud.zuul.config; + +import com.stormpath.sdk.lang.Assert; +import io.jsonwebtoken.SignatureAlgorithm; + +import java.security.Key; + +/** + * @since 1.3.0 + */ +public class DefaultJwkResult implements JwkResult { + + private final SignatureAlgorithm signatureAlgorithm; + private final Key key; + private final String keyId; + + public DefaultJwkResult(SignatureAlgorithm signatureAlgorithm, Key key, String keyId) { + Assert.notNull(signatureAlgorithm, "SignatureAlgorithm cannot be null."); + Assert.notNull(key, "Key cannot be null."); + this.signatureAlgorithm = signatureAlgorithm; + this.key = key; + this.keyId = keyId; + } + + @Override + public SignatureAlgorithm getSignatureAlgorithm() { + return this.signatureAlgorithm; + } + + @Override + public Key getKey() { + return this.key; + } + + @Override + public String getKeyId() { + return this.keyId; + } +} diff --git a/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/main/java/com/stormpath/spring/cloud/zuul/config/DisabledResourceKeyResolver.java b/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/main/java/com/stormpath/spring/cloud/zuul/config/DisabledResourceKeyResolver.java new file mode 100644 index 0000000000..17ccf6f889 --- /dev/null +++ b/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/main/java/com/stormpath/spring/cloud/zuul/config/DisabledResourceKeyResolver.java @@ -0,0 +1,32 @@ +/* + * Copyright 2017 Stormpath, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.stormpath.spring.cloud.zuul.config; + +import com.stormpath.sdk.lang.Function; +import org.springframework.core.io.Resource; + +import java.security.Key; + +/** + * @since 1.3.0 + */ +public class DisabledResourceKeyResolver implements Function { + + @Override + public Key apply(Resource resource) { + throw new IllegalArgumentException("This implementation is disabled."); + } +} diff --git a/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/main/java/com/stormpath/spring/cloud/zuul/config/JwkConfig.java b/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/main/java/com/stormpath/spring/cloud/zuul/config/JwkConfig.java index 40ad89397c..acec960594 100644 --- a/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/main/java/com/stormpath/spring/cloud/zuul/config/JwkConfig.java +++ b/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/main/java/com/stormpath/spring/cloud/zuul/config/JwkConfig.java @@ -15,6 +15,8 @@ */ package com.stormpath.spring.cloud.zuul.config; +import org.springframework.core.io.Resource; + /** * @since 1.3.0 */ @@ -28,6 +30,8 @@ public class JwkConfig { private String value; + private Resource resource; + private String id; public JwkConfig() { @@ -58,6 +62,14 @@ public void setEncoding(String encoding) { this.encoding = encoding; } + public Resource getResource() { + return resource; + } + + public void setResource(Resource resource) { + this.resource = resource; + } + public String getValue() { return value; } diff --git a/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/main/java/com/stormpath/spring/cloud/zuul/config/JwkResult.java b/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/main/java/com/stormpath/spring/cloud/zuul/config/JwkResult.java new file mode 100644 index 0000000000..b22b03369b --- /dev/null +++ b/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/main/java/com/stormpath/spring/cloud/zuul/config/JwkResult.java @@ -0,0 +1,32 @@ +/* + * Copyright 2017 Stormpath, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.stormpath.spring.cloud.zuul.config; + +import io.jsonwebtoken.SignatureAlgorithm; + +import java.security.Key; + +/** + * @since 1.3.0 + */ +public interface JwkResult { + + SignatureAlgorithm getSignatureAlgorithm(); + + Key getKey(); + + String getKeyId(); +} diff --git a/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/main/java/com/stormpath/spring/cloud/zuul/config/JwtConfig.java b/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/main/java/com/stormpath/spring/cloud/zuul/config/JwtConfig.java index d6a80c33a5..a28c972337 100644 --- a/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/main/java/com/stormpath/spring/cloud/zuul/config/JwtConfig.java +++ b/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/main/java/com/stormpath/spring/cloud/zuul/config/JwtConfig.java @@ -46,6 +46,7 @@ public class JwtConfig { public JwtConfig() { this.enabled = true; + this.key = new JwkConfig(); this.valueClaim = new ValueClaimConfig(); } diff --git a/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/main/java/com/stormpath/spring/cloud/zuul/config/PemResourceKeyResolver.java b/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/main/java/com/stormpath/spring/cloud/zuul/config/PemResourceKeyResolver.java new file mode 100644 index 0000000000..c3a857dbb2 --- /dev/null +++ b/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/main/java/com/stormpath/spring/cloud/zuul/config/PemResourceKeyResolver.java @@ -0,0 +1,119 @@ +/* + * Copyright 2017 Stormpath, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.stormpath.spring.cloud.zuul.config; + +import com.stormpath.sdk.lang.Function; +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.openssl.PEMKeyPair; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; +import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo; +import org.springframework.core.io.Resource; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.security.Key; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +/** + * @since 1.3.0 + */ +public class PemResourceKeyResolver implements Function { + + public static final String PEM_PREFIX = "-----BEGIN "; + + private final JcaX509CertificateConverter x509Converter = new JcaX509CertificateConverter().setProvider("BC"); + + private final JcaPEMKeyConverter pemKeyConverter = new JcaPEMKeyConverter().setProvider("BC"); + + private final boolean findPrivate; + + public PemResourceKeyResolver(boolean findPrivate) { + this.findPrivate = findPrivate; + } + + @Override + public Key apply(Resource resource) { + try { + return doApply(resource); + } catch (IOException | CertificateException e) { + String msg = "Unable to parse resource [" + resource + "]: " + e.getMessage(); + throw new IllegalArgumentException(msg, e); + } + } + + private Key doApply(Resource resource) throws IOException, CertificateException { + + try (Reader reader = new BufferedReader(new InputStreamReader(resource.getInputStream(), "UTF-8"))) { + + PEMParser pemParser = new PEMParser(reader); + + Object o; + + boolean encryptedPrivateFound = false; + + while ((o = pemParser.readObject()) != null) { + + if (o instanceof PKCS8EncryptedPrivateKeyInfo) { + encryptedPrivateFound = true; + } + + if (o instanceof PEMKeyPair) { + PEMKeyPair pemKeyPair = (PEMKeyPair) o; + return findPrivate ? + pemKeyConverter.getPrivateKey(pemKeyPair.getPrivateKeyInfo()) : + pemKeyConverter.getPublicKey(pemKeyPair.getPublicKeyInfo()); + } + + if (o instanceof PrivateKeyInfo && findPrivate) { + PrivateKeyInfo privateKeyInfo = (PrivateKeyInfo) o; + return pemKeyConverter.getPrivateKey(privateKeyInfo); + } + + if (o instanceof SubjectPublicKeyInfo && !findPrivate) { + SubjectPublicKeyInfo info = (SubjectPublicKeyInfo) o; + return pemKeyConverter.getPublicKey(info); + } + + if (o instanceof X509CertificateHolder && !findPrivate) { + X509CertificateHolder holder = (X509CertificateHolder) o; + X509Certificate cert = x509Converter.getCertificate(holder); + return cert.getPublicKey(); + } + } + + //if we haven't returned yet, we couldn't find a key based on our preferences. + + String msg; + + if (encryptedPrivateFound && findPrivate) { + msg = "Key resource [" + resource + "] contains a PKCS8 Encrypted PrivateKey. Only unencrypted " + + "private keys are supported."; + } else { + msg = "Key resource [" + resource + "] did not contain a " + (findPrivate ? "private " : "public ") + + "key."; + } + + throw new IllegalArgumentException(msg); + } + } +} diff --git a/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/test/groovy/com/stormpath/spring/cloud/zuul/autoconfigure/ClientApplicationJwkFactoryTest.groovy b/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/test/groovy/com/stormpath/spring/cloud/zuul/autoconfigure/ClientApplicationJwkFactoryTest.groovy new file mode 100644 index 0000000000..b3bba0c829 --- /dev/null +++ b/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/test/groovy/com/stormpath/spring/cloud/zuul/autoconfigure/ClientApplicationJwkFactoryTest.groovy @@ -0,0 +1,101 @@ +/* + * Copyright 2017 Stormpath, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.stormpath.spring.cloud.zuul.autoconfigure + +import com.stormpath.sdk.api.ApiKey +import com.stormpath.sdk.application.Application +import com.stormpath.sdk.client.Client +import io.jsonwebtoken.SignatureAlgorithm +import io.jsonwebtoken.impl.TextCodec +import java.security.SecureRandom +import org.testng.annotations.Test +import static org.easymock.EasyMock.* +import static org.testng.Assert.* + +/** + * @since 1.3.0 + */ +class ClientApplicationJwkFactoryTest { + + private static final Random RANDOM = new SecureRandom() + + @Test(expectedExceptions = [IllegalArgumentException]) + void testNullClient() { + new ClientApplicationJwkFactory(null, createMock(Application)) + } + + @Test(expectedExceptions = [IllegalArgumentException]) + void testNullApplication() { + new ClientApplicationJwkFactory(createMock(Client), null) + } + + @Test + void testNullSigAlg32ByteKey() { + testSigAlgWithNByteKey(32, null, SignatureAlgorithm.HS256) + } + + @Test + void testNullSigAlg48ByteKey() { + testSigAlgWithNByteKey(48, null, SignatureAlgorithm.HS384) + } + + @Test + void testNullSigAlg64ByteKey() { + testSigAlgWithNByteKey(64, null, SignatureAlgorithm.HS512) + } + + @Test + void testRsaSigAlg32ByteKey() { + testSigAlgWithNByteKey(32, SignatureAlgorithm.RS256, SignatureAlgorithm.HS256) + } + + @Test + void testRsaSigAlg48ByteKey() { + testSigAlgWithNByteKey(48, SignatureAlgorithm.RS384, SignatureAlgorithm.HS384) + } + + @Test + void testRsaSigAlg64ByteKey() { + testSigAlgWithNByteKey(64, SignatureAlgorithm.RS512, SignatureAlgorithm.HS512) + } + + void testSigAlgWithNByteKey(int numBytes, SignatureAlgorithm specified, SignatureAlgorithm expected) { + + def app = createMock(Application) + def client = createMock(Client) + def apiKey = createMock(ApiKey) + + def apiBaseUrl = 'https://api.stormpath.com/v1' + def apiKeyId = 'myApiKeyId' + byte[] secret = new byte[numBytes]; + RANDOM.nextBytes(secret) + expect(apiKey.getSecret()).andStubReturn(TextCodec.BASE64.encode(secret)) + expect(client.getApiKey()).andReturn(apiKey) + expect(apiKey.getId()).andStubReturn(apiKeyId) + expect(app.getHref()).andStubReturn(apiBaseUrl + "/applications/myAppId"); + + replay app, client, apiKey + + def result = new ClientApplicationJwkFactory(client, app).apply(specified) + + assertNotNull result + assertEquals result.keyId, apiBaseUrl + '/apiKeys/' + apiKeyId + assertEquals result.getKey().getEncoded(), secret + assertEquals result.signatureAlgorithm, expected + + verify app, client, apiKey + } +} diff --git a/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/test/groovy/com/stormpath/spring/cloud/zuul/autoconfigure/ConfigJwkFactoryTest.groovy b/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/test/groovy/com/stormpath/spring/cloud/zuul/autoconfigure/ConfigJwkFactoryTest.groovy new file mode 100644 index 0000000000..12ab602f52 --- /dev/null +++ b/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/test/groovy/com/stormpath/spring/cloud/zuul/autoconfigure/ConfigJwkFactoryTest.groovy @@ -0,0 +1,324 @@ +/* + * Copyright 2017 Stormpath, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.stormpath.spring.cloud.zuul.autoconfigure + +import com.stormpath.sdk.lang.DefaultRuntimeEnvironment +import com.stormpath.sdk.lang.Function +import com.stormpath.sdk.lang.RuntimeEnvironment +import com.stormpath.spring.cloud.zuul.config.DefaultJwkResult +import com.stormpath.spring.cloud.zuul.config.JwkConfig +import com.stormpath.spring.cloud.zuul.config.JwkResult +import io.jsonwebtoken.SignatureAlgorithm +import io.jsonwebtoken.impl.TextCodec +import org.springframework.core.io.ClassPathResource +import org.springframework.core.io.InputStreamResource +import org.springframework.core.io.Resource +import org.testng.annotations.Test + +import javax.crypto.spec.SecretKeySpec +import java.nio.charset.StandardCharsets +import java.security.Key +import java.security.PrivateKey +import java.security.SecureRandom +import java.security.interfaces.ECKey +import java.security.interfaces.RSAKey + +import static org.testng.Assert.* + +import static io.jsonwebtoken.SignatureAlgorithm.* + +/** + * @since 1.3.0 + */ +class ConfigJwkFactoryTest { + + private static final RuntimeEnvironment RUNENV = DefaultRuntimeEnvironment.INSTANCE; + private static final Random RANDOM = new SecureRandom() + + private static Function defaultKeyFn() { + return new Function() { + @Override + JwkResult apply(SignatureAlgorithm signatureAlgorithm) { + byte[] secret = new byte[32] + RANDOM.nextBytes(secret) + def alg = HS256 + return new DefaultJwkResult(alg, new SecretKeySpec(secret, alg.jcaName), 'foo') + } + } + } + + @Test(expectedExceptions = [IllegalArgumentException]) + void testNullRuntimeEnvironment() { + new ConfigJwkFactory(null, defaultKeyFn()) + } + + @Test(expectedExceptions = [IllegalArgumentException]) + void testNullApplication() { + new ConfigJwkFactory(RUNENV, null) + } + + @Test + void testDefaultKeyFunction() { + def jwk = new JwkConfig() + def result = new ConfigJwkFactory(RUNENV, defaultKeyFn()).apply(jwk) + assertNotNull result + assertEquals result.keyId, 'foo' + assertEquals result.getSignatureAlgorithm(), HS256 + assertTrue result.key.getEncoded().length == 32 + } + + @Test + void testHS256KeyString() { + testSymmetricKey(new JwkConfig(value: newKeyString(32)), HS256) + } + + @Test + void testHS384KeyString() { + testSymmetricKey(new JwkConfig(value: newKeyString(48)), HS384) + } + + @Test + void testHS512KeyString() { + testSymmetricKey(new JwkConfig(value: newKeyString(64)), HS512) + } + + @Test + void testKeyIdPreserved() { + testSymmetricKey(new JwkConfig(value: newKeyString(32), id: 'foo'), HS256) + } + + @Test + void testHS256WithAlg() { + testSymmetricKey(new JwkConfig(value: newKeyString(32), id: 'foo', alg: 'HS256'), HS256) + } + + @Test + void testHS384WithAlg() { + testSymmetricKey(new JwkConfig(value: newKeyString(32), id: 'foo', alg: 'HS384'), HS384) + } + + @Test + void testHS512WithAlg() { + testSymmetricKey(new JwkConfig(value: newKeyString(32), id: 'foo', alg: 'HS512'), HS512) + } + + @Test(expectedExceptions = [IllegalArgumentException]) + void testHS256WithUnknownAlg() { + testSymmetricKey(new JwkConfig(value: newKeyString(32), id: 'foo', alg: 'unknown'), HS256) + } + + @Test(expectedExceptions = [IllegalArgumentException]) + void testBothKeyValueAndResourcePathSpecified() { + def dummyResource = new InputStreamResource(new ByteArrayInputStream("test".getBytes(StandardCharsets.UTF_8))) + testSymmetricKey(new JwkConfig(value: newKeyString(32), resource: dummyResource), HS256) + } + + @Test + void testUnsupportedKeyStringEncoding() { + byte[] keyBytes = newKeyBytes(32) + String keyString = TextCodec.BASE64.encode(keyBytes) + def enc = 'whatever' + def jwk = new JwkConfig(value: keyString, encoding: "whatever") + try { + new ConfigJwkFactory(RUNENV, defaultKeyFn()).apply(jwk) + fail("Exception expected.") + } catch (IllegalArgumentException iae) { + assertEquals iae.getMessage(), "Unsupported encoding '$enc'. Supported encodings: base64url, base64, utf8, pem." + } + } + + @Test + void testBase64KeyString() { + byte[] keyBytes = newKeyBytes(32) + String keyString = TextCodec.BASE64.encode(keyBytes) + def jwk = new JwkConfig(value: keyString, encoding: "base64") + def result = new ConfigJwkFactory(RUNENV, defaultKeyFn()).apply(jwk) + assertEquals result.keyId, jwk.id + assertEquals result.key.encoded, TextCodec.BASE64.decode(jwk.value) + assertEquals result.signatureAlgorithm, HS256 + } + + @Test + void testSymmetricKeyWithoutHmacAlg() { + byte[] keyBytes = newKeyBytes(32) + String keyString = TextCodec.BASE64URL.encode(keyBytes) + def jwk = new JwkConfig(value: keyString, alg: RS256) + try { + new ConfigJwkFactory(RUNENV, defaultKeyFn()).apply(jwk) + } catch (IllegalArgumentException iae) { + assertTrue iae.getMessage().contains("stormpath.zuul.account.header.jwt.key.value") && + iae.getMessage().contains("stormpath.zuul.account.header.jwt.key.alg") + } + } + + @Test + void testUtf8KeyString() { + byte[] keyBytes = newKeyBytes(32) + String keyString = new String(keyBytes, StandardCharsets.UTF_8) + def jwk = new JwkConfig(value: keyString, encoding: "utf8") + def result = new ConfigJwkFactory(RUNENV, defaultKeyFn()).apply(jwk) + assertEquals result.keyId, jwk.id + assertEquals result.key.encoded, jwk.value.getBytes(StandardCharsets.UTF_8) + } + + @Test + void testPemKeyString() { + def resource = new ClassPathResource("rsatest.priv.pem") + def scanner = new Scanner(resource.getInputStream()).useDelimiter("\\A"); + String keyString = scanner.hasNext() ? scanner.next() : ""; + + def jwk = new JwkConfig(value: keyString) + def result = new ConfigJwkFactory(RUNENV, defaultKeyFn()).apply(jwk) + assertEquals result.keyId, jwk.id + assertNotNull result.key + assertTrue result.key instanceof RSAKey && result.key instanceof PrivateKey + assertEquals result.signatureAlgorithm, RS256 + } + + private byte[] newKeyBytes(int numBytes) { + byte[] secret = new byte[numBytes] + RANDOM.nextBytes(secret) + return secret; + } + + private String newKeyString(int numBytes) { + byte[] secret = newKeyBytes(numBytes); + return TextCodec.BASE64URL.encode(secret); + } + + + private JwkResult testSymmetricKey(JwkConfig jwk, SignatureAlgorithm expectedAlg) { + def result = new ConfigJwkFactory(RUNENV, defaultKeyFn()).apply(jwk) + assertEquals result.keyId, jwk.id + assertEquals result.key.encoded, TextCodec.BASE64URL.decode(jwk.value) + assertEquals result.signatureAlgorithm, expectedAlg + return result + } + + @Test + void testRsaPrivateKeyResource() { + def jwk = new JwkConfig(resource: new ClassPathResource("rsatest.priv.pem")) + def result = new ConfigJwkFactory(RUNENV, defaultKeyFn()).apply(jwk) + assertEquals result.keyId, jwk.id + assertTrue result.key instanceof RSAKey && result.key instanceof PrivateKey + assertEquals result.signatureAlgorithm, RS256 + } + + @Test + void testRsaPrivateKeyResourceWithInvalidAlg() { + def alg = 'ES256' + def jwk = new JwkConfig(resource: new ClassPathResource("rsatest.priv.pem"), alg: alg) + try { + new ConfigJwkFactory(RUNENV, defaultKeyFn()).apply(jwk) + fail("Exception expected.") + } catch (IllegalArgumentException iae) { + assertEquals iae.getMessage(), "Signature algorithm [$alg] is not compatible with the specified RSA key." + } + } + + @Test + void testRsaPublicKeyResource() { //public keys should never be used to sign something + def path = "rsatest.pub.pem" + def jwk = new JwkConfig(resource: new ClassPathResource(path)) + try { + new ConfigJwkFactory(RUNENV, defaultKeyFn()).apply(jwk) + fail("Exception expected.") + } catch (IllegalArgumentException iae) { + assertEquals iae.getMessage(), "Key resource [class path resource [$path]] did not contain a private key." + } + } + + @Test + void testEllipticCurvePrivateKeyResource() { + def jwk = new JwkConfig(resource: new ClassPathResource("secp384r1.priv.pem")) + def result = new ConfigJwkFactory(RUNENV, defaultKeyFn()).apply(jwk) + assertEquals result.keyId, jwk.id + assertTrue result.key instanceof ECKey && result.key instanceof PrivateKey + assertEquals result.signatureAlgorithm, ES256 + } + + @Test + void testEllipticCurvePrivateKeyResourceWithInvalidAlg() { + def alg = 'RS256' + def jwk = new JwkConfig(resource: new ClassPathResource("secp384r1.priv.pem"), alg: alg) + try { + new ConfigJwkFactory(RUNENV, defaultKeyFn()).apply(jwk) + fail("Exception expected.") + } catch (IllegalArgumentException iae) { + assertEquals iae.getMessage(), "Signature algorithm [$alg] is not compatible with the specified Elliptic Curve key." + } + } + + @Test(expectedExceptions = [IllegalStateException]) + void testResourceKeyResolverReturningNull() { + def jwk = new JwkConfig(resource: new ClassPathResource("secp384r1.priv.pem")) + def factory = new ConfigJwkFactory(RUNENV, defaultKeyFn()) { + @Override + protected Function createResourceKeyFunction(Resource keyResource, boolean keyStringSpecified) { + return new Function() { + @Override + Key apply(Resource resource) { + return null; + } + } + } + } + factory.apply(jwk); + } + + @Test + void testResourceKeyResolverReturnsUnsupportedKey() { + def jwk = new JwkConfig(resource: new ClassPathResource("secp384r1.priv.pem")) + def factory = new ConfigJwkFactory(RUNENV, defaultKeyFn()) { + @Override + protected Function createResourceKeyFunction(Resource keyResource, boolean keyStringSpecified) { + return new Function() { + @Override + Key apply(Resource resource) { + return new SecretKeySpec(newKeyBytes(32), HS256.jcaName) + } + } + } + } + try { + factory.apply(jwk); + fail("Exception expected.") + } catch (IllegalArgumentException iae) { + //ensure we're being helpful by telling them which property to configure to avoid this problem: + assertEquals iae.message, "Unable to detect jwt signing key type to provide a default signature " + + "algorithm. Please specify the stormpath.zuul.account.header.jwt.key.alg property." + } + } + + @Test + void testRsaPrivateKeyResourceWithBouncyCastleUnavailable() { + def jwk = new JwkConfig(resource: new ClassPathResource("rsatest.priv.pem")) + def runtimeEnvironment = new RuntimeEnvironment() { + @Override + boolean isClassAvailable(String fqcn) { + return false //simulate a failed lookup + } + } + try { + new ConfigJwkFactory(runtimeEnvironment, defaultKeyFn()).apply(jwk) + fail("Should have thrown an exception.") + } catch (IllegalStateException iae) { + //ensure we're helpful and tell the app developer which dependency to include: + assertTrue(iae.getMessage().contains('org.bouncycastle:bcpkix-jdk15on:1.56')) + } + } + +} diff --git a/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/test/resources/README.md b/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/test/resources/README.md new file mode 100644 index 0000000000..d1cb55df1e --- /dev/null +++ b/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/test/resources/README.md @@ -0,0 +1,27 @@ +The keys in this directory are for testing purposes only, never use them in a real application. + +## RSA Test Keys + +The `rsatest.priv.pem` RSA private key file was generated via the following: + + $ openssl genrsa -out rsatest.priv.pem 2048 + +That private key's corresponding `rsatest.pub.pem` public key was derived via: + + $ openssl rsa -in rsatest.priv.pem -pubout > rsatest.pub.pem + + +## Elliptic Curve Test Keys + +The `secp384r1.priv.pem` Elliptic Curve private key file was generated via: + + $ openssl ecparam -name secp384r1 -genkey -noout -out secp384r1.priv.pem + +Note that this explicitly references the EC curve name `secp384r1`. + +For JWT's `ES256`, `ES384` and `ES512` signature algorithms, the +respective OpenSSL curve names are `secp256k1`, `secp384r1` and `secp512r1`. + +That private key's corresponding `secp384r1.pub.pem` public key was derived via: + + $ openssl ec -in secp384r1.priv.pem -pubout -out secp384r1.pub.pem \ No newline at end of file diff --git a/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/test/resources/rsatest.priv.pem b/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/test/resources/rsatest.priv.pem new file mode 100644 index 0000000000..37be78c8cc --- /dev/null +++ b/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/test/resources/rsatest.priv.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA9K1RO7ABH1CdCDflO/V2JesfKGRDdeuyJQe4OqkTHR1LOcLP +KrCrnu+zYf1cLxemwgcbRY3RQAwJsMXNVT90kA2zfa4BgtGucckS1yTDTrrgQhFs +t16fw+fNQLvuGHRY5xshV9wrFXDsX71GHKXoA2QhBPtSGw1yiYfPtDwOvQ9jxBQk +xVJ6YsCk9nqoU5oSIsA0rRC995RAoNqo1DAjX5OhPqaOg2+3y6fV8fGI+xprEmv1 +owFiq/NFY5PtyOn8W0jSsIKLDPSogzYfeAR+Ryw0iSl5rHmLeEn16oy1aeKsApg1 +CkGTXOY7VyH0PsziTuLlOufZ1HyuGPUv7hQeUwIDAQABAoIBAQCJEou+v4Rxcaz3 +jLDcnU/6QDVtYHY2mrtraV65ZjzyA5ZAHrYGuYD8AldhXxoEu+BNNMP/fEqs8dF/ ++eBlkK4Rgct7bj8kdamfz0DBzLOp6KF4AeEA/X7Nto/TYzUo+A1SM23DlfGBCokx +vYyIwh0vwSmKa+18gFUZXT9sPnUXTm5jrfrXpRDyHyk7Kc7+2MAUkypoR9b9Qkmv +JVmDT6UJtWqOxAx+xanK475IZoz6rC5WLffQ+oDrOToJO6FMnB6jh1vuPpkaMZ4r +vqmhSAMIqFy3F4gf3IYUCLehz68NSvrcdz136tIxfIdBUtB6eONpcO8RtbqtJygX +xENgPXfBAoGBAP7GU73H3piA1U+QeszxfOGmxnPQ6SrwRsyc+kPd90fi59pRQeFS +dXOZfDNiWOUREx4QUB9WGnuvNqmOHV975psr+y1sgZW0+azIwgvhyGOdGZ+jxU6W +fDRJbpDiMpP6ywWQtQn4mPXZUreFL1m9Ix1kCySoHS30NsTpgsx9rMihAoGBAPXa +juLTlvNXNIB645+qRL7ggx8Hd4Gza3+mQT1U7iEZZ9AhQic6PHQfONqZUwCEHZB5 +DXsCFyWIm0x2EqtVSzPy5kJcl67oW4mnVCOm9SfNlqBrLGD5frRBufME/vxBjc6n +JMKcZ6ocgkCjzy8ZIIHC/eckOJ67xcXwclh5fz5zAoGAYcY9FwUgYQh4VHuPFR3M +HlFBserHwQnLMfVAelEx+C2VawxqKw3ZM08BAjtJAEfoPU5nYU9LBJJ+eN2oWh+T +pZNgZtNQe+KjOvMkvSieHdSJo+FW9Ez+R5ayzvlwDahex7j8MWJtWVRY0UNUo6zZ +UAs3146I/DzP1AwFfXLxn2ECgYBsYYQZ9IMYFTp85S/RZENYDitfk3AYilr6c/VQ +r08m4kdEllTObDrYSidLHspbcOKDnQnXT02a60TjCS4jv78eUJc3bBAmOCKaZVyP +NvveJyCe6YAv4+z6U/tAadRqqg90qXRoIoEEmfrFujEMpzwQWECMFAit2UNPhjcy +T6VLhwKBgQD7xpcvkDjx5UKzB5/yybcPSUipPaQl8gAPLV3kjDluF49eDPMu+afb +GLpheRkIpWfrCesYoBoJdb/CngiJ4sDaMncQRnGSmGjrZU9lBGN7UvaEDbJhZvej +S5RJw6iMo1PLd+ikOaFTpbsFt89l8x00A7fQu1fqmvg6CQd+gIQDtg== +-----END RSA PRIVATE KEY----- diff --git a/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/test/resources/rsatest.pub.pem b/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/test/resources/rsatest.pub.pem new file mode 100644 index 0000000000..84cd3fb94d --- /dev/null +++ b/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/test/resources/rsatest.pub.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9K1RO7ABH1CdCDflO/V2 +JesfKGRDdeuyJQe4OqkTHR1LOcLPKrCrnu+zYf1cLxemwgcbRY3RQAwJsMXNVT90 +kA2zfa4BgtGucckS1yTDTrrgQhFst16fw+fNQLvuGHRY5xshV9wrFXDsX71GHKXo +A2QhBPtSGw1yiYfPtDwOvQ9jxBQkxVJ6YsCk9nqoU5oSIsA0rRC995RAoNqo1DAj +X5OhPqaOg2+3y6fV8fGI+xprEmv1owFiq/NFY5PtyOn8W0jSsIKLDPSogzYfeAR+ +Ryw0iSl5rHmLeEn16oy1aeKsApg1CkGTXOY7VyH0PsziTuLlOufZ1HyuGPUv7hQe +UwIDAQAB +-----END PUBLIC KEY----- diff --git a/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/test/resources/secp384r1.priv.pem b/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/test/resources/secp384r1.priv.pem new file mode 100644 index 0000000000..0c1f8227fb --- /dev/null +++ b/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/test/resources/secp384r1.priv.pem @@ -0,0 +1,6 @@ +-----BEGIN EC PRIVATE KEY----- +MIGjAgEBBC92z4hOc4k1OWWlVHhNXqpXgeZvajIPAy1Y4HSgbi2dCVnl2fnePUyT +HSAkhXqXEqAHBgUrgQQAIqFkA2IABJqEhTSo3jWGw0NQibaAWW91i8UuVaLEYt2t +MWsLZKIu9za4sGBeQBZv5TgnrbN6mKrC6ro0MYb2m40D9mezqDJYqgeGDHzT/ogA +x95LcAMfWg5WjuUqJ1aWYVZJgHULag== +-----END EC PRIVATE KEY----- diff --git a/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/test/resources/secp384r1.pub.pem b/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/test/resources/secp384r1.pub.pem new file mode 100644 index 0000000000..fe75022972 --- /dev/null +++ b/extensions/spring/cloud/stormpath-zuul-spring-cloud-starter/src/test/resources/secp384r1.pub.pem @@ -0,0 +1,5 @@ +-----BEGIN PUBLIC KEY----- +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEmoSFNKjeNYbDQ1CJtoBZb3WLxS5VosRi +3a0xawtkoi73NriwYF5AFm/lOCets3qYqsLqujQxhvabjQP2Z7OoMliqB4YMfNP+ +iADH3ktwAx9aDlaO5SonVpZhVkmAdQtq +-----END PUBLIC KEY----- diff --git a/pom.xml b/pom.xml index 7f8b006a99..edd64e9e52 100644 --- a/pom.xml +++ b/pom.xml @@ -95,6 +95,7 @@ 1.7 1.7.21 + 1.56 3.6.4 4.5.2 2.8.1 @@ -344,6 +345,12 @@ org.apache.oltu.oauth2.resourceserver ${oltu.version} + + org.bouncycastle + bcpkix-jdk15on + ${bouncycastle.version} + true + com.hazelcast hazelcast