CrunchyBagel / TracePrivately

A privacy-focused app using Apple's soon-to-be-released contact tracing framework.
MIT License
350 stars 27 forks source link

Ability to authenticate with the server #29

Closed HendX closed 4 years ago

HendX commented 4 years ago

In order to ensure only the app communicates with the server, it should be possible to authentication.

In the feat/server_authentication branch (3176a966f94c6c5feec1e39b61531714a1b24c4e) I've implemented this in a modular way.

The workflow of this is:

1) The specific authentication to use is specified in KeyServer.plist 2) There is now an auth endpoint required on the server in order to use token validation 3) Forks can easily create their own authentication module (and these can be merged back in) 4) If the app receives a 401 Unauthorized and authentication is configured, then it will request a token from the server 5) App will store and retrieve the token automatically 6) The server auth endpoint should read the bearer token from the HTTP headers and act accordingly. Likewise, the other endpoints should validate this token. The token format is not specified.

(currently only "receipt validation" option is available, which submits the App Store receipt to the auth endpoint)

HendX commented 4 years ago

@tatey would you mind look at the feat/server_authentication branch and see if there are any issues? I've also updated the YAML file.

HendX commented 4 years ago

I'm wondering if there might be a better way for this system to work. This isn't my area of expertise, so just think out loud:

Perhaps instead of an auth endpoint, the device registers for push notifications on launch, then the server generates a token and delivers it via (silent) push notification. By using APNS, it would ensure on the device will ever see a token.

  1. Device registers for push
  2. Device submits to server auth with its push notification device ID
  3. Server generates token and submits it to APNS (push notification service)
  4. Device receives token from APNS for subsequent use

In this scenario, an 401 from the API would trigger this workflow again. Since this happens asynchronously, the original API call that received a 401 would then need to execute again.

tatey commented 4 years ago

Ohhh. I really like that flow. By using APNS to send the token back to the device it guarantees that only genuine clients will receive an authentication token. A non-genuine client would never receive the token because it would be using different certificates.

Further thoughts:

tatey commented 4 years ago

Being able to register for notifications without prompting was something that changed in iOS 12, right? But they go into the background and never ask for your attention. Can the user still explicitly turn those off? If they do, that would mean the app wouldn't be able to auth.

tatey commented 4 years ago

How does something like this look?

curl -X POST -H "User-Agent: "TracePrivately Version/1.0 Identifier/com.traceprivately.demo" -H "Content-Type: application/json" -d '{token: "secret"}' http://example.com/api/register

Or if you wanted to send the identifier in the body

curl -X POST -H "Content-Type: application/json" -d '{token: "secret", identifier: "com.traceprivately.demo"}' http://example.com/api/register

And then only requests to the /api/infected and /api/submit endpoints would need to send the Authorization: Bearer=<secret> header.

tatey commented 4 years ago

It would also be best practice to put some sort of expiry on the authenticating token. I'm thinking 90 days? The client could get a new one any time by hitting the /api/register endpoint. This is something you could schedule to happen locally or on 401.

tatey commented 4 years ago

Reading the Local and Remote Notification Programming Guide we're encouraged to to call registerForRemoteNotifications every launch which would trigger the above flow anyway.

kevinrmblr commented 4 years ago

Hi there,

First off, great work so far! Really inspiring to see what you are doing.

About using APNS tokens, I'm not sure if that's the best way to go. Users can deny push notification access, which would render the app unable to receive a token. Apple probably will not like forcing users to enable push notifications. Also, users could revoke access at a later point. Lastly, there's no guarantee a token will not be altered at any time.

I did some searching, if this is about checking if the device/app is genuine, the DeviceCheck API seems like it might be a good fit. In short, it allows the device to generate a temporary token that can be validated by the backend by querying apple servers.

Edit; I'm not really into Android development, but think the Android version of this would be the SafetyNet Attestation API.

HendX commented 4 years ago

Thanks @kevinrmblr - I didn't know about DeviceCheck, but at first glance that looks like it could be the way to go.

The way I've implemented the authentication so far is in a modular way, so I'll look into creating a module that sends DeviceCheck data.

A few other notes about previous comments:

1) I don't believe users can disable silent push notifications. They are not for display - the user can disable the appearance of visible push notifications. Thus I think the APNS route would likely work, but yea, there's extra points of failure.

2) And yea, you register for push notifications on every launch and get a unique ID which you submit back to the server, which it subsequently stores

3) @tatey Yea I think expiry of tokens makes sense, even shorter than that perhaps. That can be implemented in any way desired by any server implementation. Just return a 401 when it wants to force re auth

I'm wondering if in addition to the Swagger file if there needs to be a list of recommendations for server implementations. Or maybe this can all be incorporated into the Swagger.

tatey commented 4 years ago

The way I've implemented the authentication so far is in a modular way

As a reference implementation what do you think about this project making a recommendation and backing a single way of doing authentication? The code is open source right, so if a Government or health authority wants to do it differently they can just swap it out for something else themselves.

If there is an agreement between the reference client and the reference server then it would make this project more of a turn key solution.

I don't believe users can disable silent push notifications.

Okay, this actually means APNS could still be viable on iOS. I do believe on Android it's possible to turn them off completely.

And yea, you register for push notifications on every launch and get a unique ID

Again means the server can continually rotate authentication tokens with the client which means we can make their expiry shorter.

The way I've implemented the authentication so far is in a modular way, so I'll look into creating a module that sends DeviceCheck data.

@kevinrmblr Thanks a tonne. I did not know about the DeviceCheck data. This would solve the "genuineness" problem, but it will make the registration endpoint slower and more prone to breaking because the client will need to wait until the server validates the token. What happens if there's a network problem? What happens if there's a sudden flood of registrations? That said, there's definitely a simplicity to the synchronous flow.

Push notification registration flow:

Screen Shot 2020-04-25 at 12 57 50 pm

DeviceCheck registration flow:

Screen Shot 2020-04-25 at 12 58 56 pm

Honestly I still prefer the push notification flow, but I've never seen this before and as @kevinrmblr points out it might not be the spirit of these APIs.

Even though some risk exists on the server about not being able to validate the token if it gives a bad response to the client it can just change again. Still, I've learnt that you should try to put as much as you can into the background when a network is involved.

--

I'm wondering if in addition to the Swagger file if there needs to be a list of recommendations

Potentially. Does Swagger have a way of doing that? Otherwise I feel like it would best go in the README for the Key Server reference in this project.

HendX commented 4 years ago

I've added various notes to the swagger about this. I don't know if this is the best place but for now keeping it in there keeps it all together.

I've now added a DeviceCheck module to the iOS code, which can be selected using "deviceCheck" in the KeyServer.plist file for authentication. I haven't implemented any server side code to verify this, just to transmit the data.

I plan on merging the auth code into the main branch in the coming days.

tatey commented 4 years ago

Okay, so we're definitely not going down the push notification route? I can make "deviceCheck" work easily enough.

HendX commented 4 years ago

It’d need quite a bit of work, so I’d rather just wait and see if there are any specifics imposed by Apple that may impact this.

kevinrmblr commented 4 years ago

Thanks for the diagrams, @tatey, helps clarify the situation.

I can imagine from an app perspective, receiving a token upon authentication would simplify things a bit using a synchronous calls, mostly when it comes to retry scenario's where calls failed.

A nice extra from the DeviceID API, is that you're able to store two bits at the apple-side, that are bound to the device. This even works after reinstalls of the os/app. For example, abusive devices could be marked as such and blocked from doing any further calls. (This page seems to explain the setup quite nicely)

I think you're right about the push token, @HendX, registering for notifications without permissions (badge, sound, etc.) would give you a token for silent push.

Either way, making the authentication modular seems like a good way to go 👍 . I can imagine a backend can enable/disable certain ways of obtaining a token (push, deviceID, free-for-all) and the app using one of the allowed methods. As long as the response is a token, all calls after the auth-call can work the same.

Lastly, another way could be to combine both options? So similar to the 'Push notification registration flow', but before it sends back the token via silent push, use the DeviceID API to validate the device. Then it could be done asynchronously as well.

HendX commented 4 years ago

@kevinrmblr yea, you could combine both options. It would just need another authentication module that internally uses both existing modules and creates a key that consists of both.

The app changes needed for token via push would be:

1) Register for push notifications 2) Add a hook to handle push notification and store accordingly 3) Create a new auth module that subsequently uses that received data

Then, to create one that combines both:

1) Create a new auth module that internally uses both the device ID and push methods to save/load auth key data.

I'm just going to avoid going down the path of push notifications for now while we learn more about the API. Apple may have specific requirements on how data is transferred.

HendX commented 4 years ago

By the way, I've merged the authentication branch into main. it's optional to use, indicated by the KeyServer.plist file.

tatey commented 4 years ago

Okay, I'm convinced. Thanks for being patient with me @HendX and @kevinrmblr.

I have implemented this behaviour in the ruby implementation. You can try it out right now.

$ curl -s -v -X POST -H "Accept: application/json" "https://trace-privately-demo.herokuapp.com/api/auth" | jq
< HTTP/1.1 200 OK
< Content-Type: application/json; charset=utf-8
{
  "status": "OK",
  "token": "[REDACTED]",
  "expires_at": "2020-05-03T12:25:11Z"
}

The token will expire after 7 days. The endpoint is rate limited to 3 requests per 15 minutes by IP (Potentially too aggressive, but would rather start at low). I haven't implemented verifying the payload from DeviceCheck yet. That's in https://github.com/tatey/trace_privately/issues/19.

HendX commented 4 years ago

Great! I'll add expires_at to the Swagger and client app (#46)

HendX commented 4 years ago

Since there are now authentication options available, I'm going to close this issue. If we end up adding support for alternative options (such as push notifications) I think we can create a new issue and reference this one.