I am attempting to implement a service worker that performs push notifications for a web app. I'd like for the system to be resilient in that it keeps sending push notifications without the user needing to regularly interact with the app. To that end, I'm eyeing the pushsubscriptionchange event, but am not sure how to work within the service worker's limited constraints.
Method 1: post back to my app
My current code captures this event, creates a new subscription and sends it back to the client via postMessage. The client then calls my back-end and registers the new subscription endpoint.
This seems like the most elegant way, except I assume it would require an open tab. If the user doesn't check in for a few days but is still interested in getting push notifications, the existing subscription would eventually time out, and there would be no way to notify the app about the newly-acquired endpoint.
Method 2: securely call a REST endpoint
I then had the idea to add a REST endpoint to my app that accepts an updated subscription. One complication is that the app uses Meteor, so there's a substantial bit of its own infrastructure (the DDP protocol, for instance) that service workers don't have access to.
My thought was to generate a UUID as a token whenever a new subscription is added. Service workers could then fetch an endpoint passing this token as a query parameter and sending a new endpoint/key. A new token is then created, the old one deleted, and the new push endpoint is registered.
Unfortunately, I'm not sure how to store the token in a way that the clients and service workers could share. Local storage isn't available to service workers. The cache API seems targeted at actual requests, not at simple key/values. IndexDB is available to service workers, but I literally just want to store a UUID keyed off of something like "pushEndpointToken". Do I really have no other option than opening a database and declaring a schema for this single key/value?
Is there some third method I'm not considering? I just want to ensure that my app always has a valid push endpoint for as long as the user wants to receive notifications from it.
Thanks.
Update
Runtime variables are lost when service worker is stopped and woken up later. You need to use persistent storage such as IndexedDB or cookies (to append token to requests).
I can recommend idb-keyval - I'm sharing one database with between app (write) and service worker (read).
old answer:
You can use PostMessage API to send token into service worker
This is a concept, I'm still working on it
service-worker.js:
const runtimeVars = {
token: undefined,
}
self.addEventListener('message', event => {
// Probably no need for this check
if (event.origin !== self.origin) {
return
}
if (event.data.token) {
runtimeVars.token = event.data.token
}
})
self.addEventListener('pushsubscriptionchange', event => {
// Fire unregister request with runtimeVars.token
})
app.js
const controller = window.navigator.serviceWorker.controller
if (controller)
// At app dispatch
controller.postMessage({token: localStorage.getItem('token'})
// After login pass API token to service worker
controller.postMessage({token})
// After logout, nullify token
controller.postMessage({token: undefined})
}
Related
I want to use Firebase Auth for my user login/registration process. Everything else should be handled by my own backend (spring boot app + postgres db).
Now I'm asking myself how I can synchronize a new created user to my user table in postgres. I thought about the following:
REST call through client - Everytime I get a success event from the firebase sdk I call an additional request to my backend which sends uid, username etc.
Problem: What if my backend call fails but the register process was successful ? That would lead to an inconsistent state since (at least thats what I understanded) I can't easily rollback. That would lead to situations where a user can login into my app without my backend knowing the user. This would crash/ invalidate all my following queries (e.g. search after user xyz would lead to no result even though he/she exists)
Check the existence of the user in the postgres database
Here I would query the uid from the database (which I got from the jwt) and create a new user if it doesn't exists in every incoming request.
Problem: The user query is a unnessecary overhead for every incoming request.
Trigger with cloud functions - When I understood it right firebase auth is firing events when a new user is created in cloud functions. This could be used to make the external api call.
Problem: I dont know what happens when my external rest call fails at this point. Can I rollback the registration ? Will I be ever catch this event again ? I also proably would have an eventual consistency situation, since I dont know when the cloud function triggers. Furthermore I would prefer not to include cloud functions to my stack
Is there any way how I could do this in a transactional manner ? Did anyone else tried is using sth simular ?
Thanks for every help!
The easiest way is actually to not synchronize auth data, but instead decode and verify the ID token of the user in your backend code.
This operation is (by design) stateless, although Firebase's own backend services often implement a cache of recently decoded tokens to speed up future calls with the same ID token.
Apparently, I finally came up with a different solution:
Register user per Firebase SDK (e.g. with email + pw method)
Make a post-call to my own registration api including the resulting uid from the previous step and some metadata
API creates a new user including a column with the UID + Fetches the firebase token of the user and adds an internal claim that references to the internal Postgres UUID via Admin SDK.
Frontend gets the created user and hard refreshes (very important, since the previously fetched token won't contain the newly added claim !) the firebase token and verifies that it contains the token. If it does -> everything is cool, if not some oopsie happened :) That will require a request retry.
Later when you start your app you can just check if the passed token contains the custom claim, if not open the sign up/sign in page.
Every endpoint except the one for registration should check if the claim is set. If not just forbid the request.
How to set custom claims:
https://firebase.google.com/docs/auth/admin/custom-claims#set_and_validate_custom_user_claims_via_the_admin_sdk
You can use the Firebase Admin SDK to create the user account from your back-end instead of from the client.
So first you create the user in your database, then grab the ID and use it to create a user with the same ID in Firebase.
If all goes well, send a confirmation to the client and sign it in using the same credentials they entered.
Why not creating an endpoint in your backend service and call this endpoint when a client side authentication succeeds?
This method should do 2 things:
decode token to get access to Firebase user object (Firebase Admin)
Compare Firebase user with your internal user table. if it doesn't exist you can create it using firebase user object, otherwise do nothing.
This solution allows you to do other nice things as well (Syncing user info between Firebase and your internal db, providing a way to let a frontend know if this user is new or not, ...) at a relative small cost (1 get call per sign in)
The examples from here require Application type permissions, which means, that such app will have access to all mailboxes in the tenant. Which, basically, means that it can't be just any tenant, because under normal circumstances, access to all mailboxes is a no-no.
I.e., it seems to be only practical for the tenants, which are narrowly specialised in handling certain mail, and does not contain any regular individual's mail.
The question: is there, perhaps, any way to run a daemon with only delegated permissions configured for the app? That would seem to solve the predicament.
One way is to limit the daemon app's permissions in Exchange Online with an application access policy: https://learn.microsoft.com/en-us/graph/auth-limit-mailbox-access.
The other more complicated way is to use delegated permissions through refresh tokens.
The process for that looks like this:
User who wants to give the app access to send emails from their mailbox signs in to the app (you need some kind of front-end)
The app receives an ID token, an access token, and a refresh token
ID token tells the app who the user is, so they can get for example the user's unique immutable id from there (the oid claim) in order to identify the user who gave access
The access token isn't really necessary here, so the app can throw that away
The refresh token on the other hand is stored in a secret store
Daemon app wants to send an email, so it gets the refresh token from secret store
A new access token and refresh token are acquired from Azure AD using the refresh token
The new refresh token is stored in the secret store to replace the existing one
The daemon app uses the access token to send the email as the user
The daemon app needs to be ready for the inevitable situation where the refresh token doesn't work though.
It could have expired for various reasons.
In that situation the first steps need to be repeated for that user.
The refresh token can also expire if not used, though this may have changed in recent times.
In the past we made a process that would ensure that each refresh token got at least used to acquire new tokens once a week to keep them fresh.
So you can see that the daemon app approach with application permissions is a lot simpler and more robust (just have to make sure the secret/certificate doesn't expire).
On the other hand the approach with delegated permissions is fundamentally more secure as you can't gain more access than what the user has.
Individual users can also revoke consent.
If you are just trying to read one particular AD user's mailbox, you may be able to use the Integrated Windows provider, which uses delegated permissions. While this is meant more for desktop/mobile apps, it can be used in daemon apps as well. The nice thing about it is it allows you to get a token silently, with no user interaction required. There are a few caveats, however, which admittedly limits its scope:
The app must be running as the user whose mailbox you are trying to access.
The user cannot have MFA enabled (since that requires user interaction--may not be an issue for a service account user).
You would need to enable the "Allow public client flows" app setting in the Azure portal.
The code would then look something like below. More info here and here.
AuthenticationConfig config = AuthenticationConfig.ReadFromJsonFile("appsettings.json");
IPublicClientApplication app = PublicClientApplicationBuilder
.Create(config.ClientId)
.WithTenantId(config.Tenant)
.Build();
string[] scopes = new string[] { $"{config.ApiUrl}.default" };
var authProvider = new Microsoft.Graph.DelegateAuthenticationProvider(async (request) => {
var result = await app.AcquireTokenByIntegratedWindowsAuth(scopes).ExecuteAsync();
request.Headers.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", result.AccessToken);
});
var graphClient = new Microsoft.Graph.GraphServiceClient(authProvider);
var messages = await graphClient.Users["TheUser#YourDomain.com"].Messages.Request()
.Select("sender,subject").GetAsync(); // substitute your user
I implemented Firebase Phone Auth for SignIn in my ReactNative project. Now I want to use this JWTToken to be passed to the API calls that I make to my server.
And at the server side, I would be validating the token which was passed through the API calls and respond with the proper response. My question is, how can I pass this token in the API calls that I make to my server?
I can store the token (within my first loading screen of the app, where it authenticates the User) in the localStorage and fetch it later in any of my screens to make the API calls
I can access the Token directly my importing the firebase package in each and every screen (from which am planning to do the API calls) like this: https://rnfirebase.io/reference/auth/idtokenresult and pass it in the API calls
But I was thinking about storing the Token (fetched during the loading screen) in a global variable inside my ReactNative project and that can be accessed from any screens. But I couldn't find how this can be done? Or which one would be the more appropriate way to do this?
EDIT:
This is how am getting the Token :
auth().onIdTokenChanged(function(user) {
if (user) {
user.getIdToken().then( token => {
console.log( token )
});
}
});
Storing the token in local storage is not going to work out well for you in the long run. ID tokens expire after 1 hour, and will not successfully verify on the server after that.
Each individual page should set up an ID token listener so it can use the most fresh token provided by the Firebase Auth SDK. The SDK will automatically refresh it and provide you with the latest token in the callback. Every time the token changes, you should use that value in your API calls. Use onIdTokenChanged():
firebase.auth().onIdTokenChanged(function(user) {
if (user) {
// User is signed in or token was refreshed.
}
});
I'm currently working on a web app which will allow a user to subscribe to push notifications. We'll store the subscriptions in a database table mapped against the user's ID and when a notification needs to be sent, we'll look up the user's subscriptions and send the notifications.
I've been following this guide:
https://developers.google.com/web/fundamentals/codelabs/push-notifications/
All is going well, but something just doesn't feel "right".
On every page load, the service worker is registered, then it checks if they're already subscribed, then even if they are it calls updateSubscriptionOnServer() which contains the following comment:
// TODO: Send subscription to application server
This effectively means that every page load is going to be attempting to write the same subscription back to the database. Obviously we'll handle that in the application, but it doesn't seem ideal.
With the Web Push API, is this the expected approach?
Many thanks
Yes it can be useful to send the subscription to server on every page load:
The user may have changed permission from blocked to granted (or default) in the browser settings, so you want to create the subscription and send it to the server
The subscription endpoint may change for different reasons, so you need to make sure that the current endpoint is sent to the server (the previous endpoint will return 410 Gone)
A compromise between the points above and performance can be to send the subscription to the server only on given pages (e.g. homepage).
I am the new for FCM. Here are some questions about the registration token:
Is the registration token generated by the FCM connection server?
Does the token change periodically in the connection server?
When?
Will it force the onTokenRefresh() in the app to be called?
I have googled for a week but didn't get any details. Please help. Thanks.
1. Is the registration token generated by the FCM connection server?
No. It gets generated by the FirebaseInstanceID. The way I understand the flow of event on first time registration:
The app retrieves a unique Instance ID.
The registration token is generated by calling the InstanceId.getToken().
Developer (usually) sends the token to the App Server.
2. Does the token change periodically in the connection server?
I think the onTokenRefresh() docs pretty much answers this.
Called when the system determines that the tokens need to be refreshed. The application should call getToken() and send the tokens to all application servers.
This will not be called very frequently, it is needed for key rotation and to handle Instance ID changes due to:
App deletes Instance ID
App is restored on a new device
User uninstalls/reinstall the app
User clears app data
The system will throttle the refresh event across all devices to avoid overloading application servers with token updates.
See this part of the docs for more details.