parse-community / parse-server-push-adapter

A push notification adapter for Parse Server
https://parseplatform.org
MIT License
88 stars 100 forks source link

Encoding an APNs token in JSON for node-apn (parse-server) #121

Closed drdaz closed 5 years ago

drdaz commented 5 years ago

I've deployed a parse-server app in Heroku that I'd been developing locally using Docker. This app uses push notifications, and so needs access to some crypto data to play with APNs.

Locally I mounted a volume with the token key file in it. On Heroku this isn't an option, and I don't want to bundle the key in the package unencrypted. I've been trying to encode the token key in the JSON argument to the "PARSE_SERVER_PUSH" environment variable in different ways. It seems what's needed is a Buffer, but I have no idea how to represent such a thing in JSON, and I haven't found anything through some searching.

I'm currently using this at my push config:

{"ios": 
    {
        "token": {
        "key": {"type":"Buffer","data":[45, 45, ... 45]},
        "keyId": "MYKEYID",
        "teamId": "MYTEAMID" 
        },
        "topic": "com.myApp", 
        "production": false
    }
}

But the server chokes on the 'key' field:

2019-01-23T19:47:36.725181+00:00 app[web.1]: VError: Failed loading token key: path must be a string or Buffer
2019-01-23T19:47:36.725191+00:00 app[web.1]:     at prepareToken (/parse-server/node_modules/apn/lib/credentials/token/prepare.js:15:13)
2019-01-23T19:47:36.725193+00:00 app[web.1]:     at config (/parse-server/node_modules/apn/lib/config.js:43:31)
2019-01-23T19:47:36.725194+00:00 app[web.1]:     at new Client (/parse-server/node_modules/apn/lib/client.js:21:19)
2019-01-23T19:47:36.725195+00:00 app[web.1]:     at new Provider (/parse-server/node_modules/apn/lib/provider.js:12:19)
2019-01-23T19:47:36.725201+00:00 app[web.1]:     at Function._createProvider (/parse-server/node_modules/@parse/push-adapter/lib/APNS.js:251:22)
2019-01-23T19:47:36.725203+00:00 app[web.1]:     at new APNS (/parse-server/node_modules/@parse/push-adapter/lib/APNS.js:81:29)
2019-01-23T19:47:36.725204+00:00 app[web.1]:     at new ParsePushAdapter (/parse-server/node_modules/@parse/push-adapter/lib/ParsePushAdapter.js:65:40)
2019-01-23T19:47:36.725205+00:00 app[web.1]:     at loadAdapter (/parse-server/lib/Adapters/AdapterLoader.js:31:16)
2019-01-23T19:47:36.725206+00:00 app[web.1]:     at loadAdapter (/parse-server/lib/Adapters/AdapterLoader.js:24:12)
2019-01-23T19:47:36.725207+00:00 app[web.1]:     at getPushController (/parse-server/lib/Controllers/index.js:230:54)

The documentation (https://github.com/node-apn/node-apn/blob/master/doc/provider.markdown) suggests it's possible to encode the token data directly in the JSON:

token.key {Buffer|String} The filename of the provider token key (as supplied by Apple) to load from disk, or a Buffer/String containing the key data.

Any ideas?

flovilmart commented 5 years ago

Can you try to put the key as the data do the buffer as a string instead of the buffer JSON object?

drdaz commented 5 years ago

Can you try to put the key as the data do the buffer as a string instead of the buffer JSON object?

You mean something like this?

{"ios": 
    {
        "token": {
        "key": "45, 45, ... 45",
        "keyId": "MYKEYID",
        "teamId": "MYTEAMID" 
        },
        "topic": "com.myApp", 
        "production": false
    }
}

EDIT: Removed braces

drdaz commented 5 years ago

@flovilmart If I put the data in a string as above, I get the following error: Failed loading token key: ENAMETOOLONG: name too long, open

... I guess it interprets that as a filename.

flovilmart commented 5 years ago

Would it be possible for you to pass it as a path?

drdaz commented 5 years ago

I don't think so; I'd have to copy the unencrypted token to the parse-server image.

I have a feeling it's syntax. I messed around with this JSON a lot trying to pass it as an environment variable into docker.

This seems to be where the magic happens in node-apn:

function resolveCredential(value) {
  if (!value) {
    return value;
  }
  if(/-----BEGIN ([A-Z\s*]+)-----/.test(value)) {
    return value;
  }
  else if(Buffer.isBuffer(value)) {
    return value;
  }
  else {
    return fs.readFileSync(value);
  }
}
flovilmart commented 5 years ago

Where is this line in node pan?

drdaz commented 5 years ago

lib/credentials/resolve.js

That's pretty much the whole file.

flovilmart commented 5 years ago

right and your token does not begin by ---BEGIN...

I'd have to copy the unencrypted token to the parse-server image.

Ok that's because heroku does not properly support VOLUMES 😢

This is quite sad, would you be willing to open a PR on this repo? The strategy would be to try to JSON parse the key,and if it succeeds, create a Buffer instance with the underlying data.

drdaz commented 5 years ago

My token does have a first line that matches the regex. At least in an online evaluator.:

"-----BEGIN PRIVATE KEY-----"

flovilmart commented 5 years ago

I guess the codepath is not taken then, did you try locally by adding some logs?

drdaz commented 5 years ago

I didn't; I'm using the docker image. I should get some sleep; it's nearly midnight here 😴

drdaz commented 5 years ago

I've opened a similar issue on node-apn in hope somebody can shed light on this.

Although the repo doesn't look all that active :-/

flovilmart commented 5 years ago

I don’t believe the issue is with node apn. At least I am not sure that the codepath you mention is taken when you provide your key this way

drdaz commented 5 years ago

It might not be; I'll see if I can confirm I hit that code.

Regardless, it looks like a point where their documentation is either lacking or incorrect. While it's allegedly possible to do this, there are no instructions as to how to do so.

flovilmart commented 5 years ago

When you pass the JSON object of a buffer, this is not a buffer instance, as specified in the documentation.

As for Docker, you could copy the key to a file.

Do you have any cloud code?

drdaz commented 5 years ago

I could copy the key. But unlike the other crypto assets used in Apple's notifications, the .p8 files for tokens don't seem to have a passworded version that I could safely include in the docker image. Unless we consider the other 2 required fields to be security enough?

I do have Cloud Code. That's where the notifications are fired :)

flovilmart commented 5 years ago

So you could very well write your file from an environment variable, when building the image.

ARG PUSH_TOKEN
RUN echo $PUSH_TOKEN > path/to/key.p8
drdaz commented 5 years ago

Yes. But I assume the presence of that file unencrypted on the live image to be a security issue. Compromise the server, and you've got my private key.

drdaz commented 5 years ago

Although now we're talking about it... I'm not sure it makes a huge difference once the server has been compromised whether the data is on the filesystem, or available as an environment variable...

flovilmart commented 5 years ago

As an environment variable it’s the same. if your server is compromised, it’s game over. You can password protect your private key, then use an environment variable to unlock it.

Another option that you didn’t consider is to use a config.js file.

The docker image can take a configuration. File a a JS script. This configuration can then parse part of the env vars, and create a new Buffer(process.env.PUSH_TOKEN)

Do you see what I mean?

drdaz commented 5 years ago

I think I get it...

I went for the env variable option because I try and leave Parse (and all other 'off the shelf' products) as 'clean' as possible, and that seemed like the least invasive way to access the needed data. It also struck me as more secure.

I can see some of that thinking may have been a little misguided :)

flovilmart commented 5 years ago

So that would be pretty straightforward:

// config.js
module.exports = {
   // appId: "applicationId" // not needed if you pass the environment variables ;)
   // feel free to put non critical configs here
   // all PARSE_SERVER_* env vars will be taken into account
   push: {
      ios:  {
        "token": {
        "key": new Buffer(process.env.PUSH_KEY),
        "keyId": "MYKEYID",
        "teamId": "MYTEAMID" 
        },
        "topic": "com.myApp", 
        "production": false
       }
   }
}
# Dockerfile
FROM parseplatform/parse-server

# your original dockerfile
COPY config.js ./

# Pass the entrypoint this way
ENTRYPOINT ["node", "bin/parse-server", "config.js"]
drdaz commented 5 years ago

Thanks. I like that; it's definitely better than including the token in the image. This way my token isn't sitting in Heroku's repo.

As an aside, I found out yesterday (with some displeasure) that Heroku ignores ENTRYPOINT entries in Dockerfiles, and executes CMDs.

To get my Dockerfile to actually work on Heroku, I needed to include the following:

ENTRYPOINT [  ] # This to null the ENTRYPOINT from parse-server's Dockerfile

CMD ["node", "/parse-server/bin/parse-server"]

I can't for the life of me find the link containing this info now... but it's kinda tiresome they implement Docker differently 😩

flovilmart commented 5 years ago

To get my Dockerfile to actually work on Heroku, I needed to include the following:

this is kinda problematic. :/ One day, I wish we'd do a full documentation on docker and k8s and heroku etc...

for this you shuold then do:

ENTRYPOINT [  ] # This to null the ENTRYPOINT from parse-server's Dockerfile

CMD ["node", "/parse-server/bin/parse-server", "config.js"]

Let me know if this works? having a config.js file is also very powerful as you can now leverage js (reference modules and packages, use a middleware etc...)

drdaz commented 5 years ago

It certainly works. And I can see that's a handy pattern... thanks :)

JS on the whole is still a fairly new and fascinating thing for me.

One day, I wish we'd do a full documentation on docker and k8s and heroku etc...

The fact Heroku are saying they offer Docker hosting is just wrong when their hosting behaves differently to Docker 🤷🏼‍♂️ I do love their service and business model though.

I'd love to hear other EU friendly suggestions as to where to run small docker apps though, that don't require you to maintain the underlying host.

flovilmart commented 5 years ago

Glad to hear it works!

For your question, it would be better asked on the https://community.parseplatform.org forums!

Also, would be a nice topic for the forum: »deploying to heroku with Docker »

drdaz commented 5 years ago

I discovered an unfortunate feature btw; the buffer containing the token data gets dumped to the logs if VERBOSE=true. Heh.

Oooh a forum. I had no idea... nice.

flovilmart commented 5 years ago

I discovered an unfortunate feature btw; the buffer containing the token data gets dumped to the logs if VERBOSE=true. Heh.

Yeah that’s annoying, would you be willing to make a Pr to fix it? This is well isolated in the code

drdaz commented 5 years ago

Yeah I’ll take a look at it; any pointers as to where to look are appreciated :)

On 24 Jan 2019, at 15.50, Florent Vilmart notifications@github.com wrote:

I discovered an unfortunate feature btw; the buffer containing the token data gets dumped to the logs if VERBOSE=true. Heh.

Yeah that’s annoying, would you be willing to make a Pr to fix it? This is well isolated in the code

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/parse-community/parse-server-push-adapter/issues/121#issuecomment-457223765, or mute the thread https://github.com/notifications/unsubscribe-auth/AAznowpS04JSEdFdchjwXSix9tpp_0FEks5vGcg2gaJpZM4aPlAh.

flovilmart commented 5 years ago

https://github.com/parse-community/parse-server/blob/master/src/cli/utils/runner.js All there :)