-
-
Notifications
You must be signed in to change notification settings - Fork 9
/
Copy pathCustomClaims.ino
398 lines (327 loc) · 14.6 KB
/
CustomClaims.ino
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
/**
* ABOUT:
*
* The example to show how to use custom claims and UID with security rules to control Firebase Realtime database access.
*
* We use the sync functions in this example because it is easy to describe the processes stype by step.
*
* For more details of using claims with security rules to control Firebase services accesses,
* visit https://firebase.google.com/docs/auth/admin/custom-claims
*
* For security rules information.
* https://firebase.google.com/docs/rules
* https://firebase.google.com/docs/rules/rules-language
* https://firebase.google.com/docs/database/security/rules-conditions
*
* This example uses the CustomAuth class for authentication for setting our custom UID and additional claims.
*
* The LegacyToken class (database secret) was used only for security rules read and modification process.
*
* The DefaultNetwork class was used for network interface configuration.
*
* See examples/App/AppInitialization and examples/App/NetworkInterfaces for more authentication and network examples.
*
* The FirebaseJson library used in this example is available to install in IDE's Library Manager or
* can be downloaded from https://github.com/mobizt/FirebaseJson.
*
* The syntaxes used in this example are described in example/App/AppInitialization/Sync/CustomAuth/CustomAuth.ino
*
* The complete usage guidelines, please read README.md or visit https://github.com/mobizt/FirebaseClient
*
*/
#include <Arduino.h>
#if defined(ESP32) || defined(ARDUINO_RASPBERRY_PI_PICO_W) || defined(ARDUINO_GIGA) || defined(ARDUINO_OPTA)
#include <WiFi.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#elif __has_include(<WiFiNINA.h>) || defined(ARDUINO_NANO_RP2040_CONNECT)
#include <WiFiNINA.h>
#elif __has_include(<WiFi101.h>)
#include <WiFi101.h>
#elif __has_include(<WiFiS3.h>) || defined(ARDUINO_UNOWIFIR4)
#include <WiFiS3.h>
#elif __has_include(<WiFiC3.h>) || defined(ARDUINO_PORTENTA_C33)
#include <WiFiC3.h>
#elif __has_include(<WiFi.h>)
#include <WiFi.h>
#endif
#include <FirebaseClient.h>
#include <FirebaseJson.h>
#define WIFI_SSID "WIFI_AP"
#define WIFI_PASSWORD "WIFI_PASSWORD"
// The API key can be obtained from Firebase console > Project Overview > Project settings.
#define API_KEY "Web_API_KEY"
/**
* This information can be taken from the service account JSON file.
*
* To download service account file, from the Firebase console, goto project settings,
* select "Service accounts" tab and click at "Generate new private key" button
*/
#define FIREBASE_PROJECT_ID "PROJECT_ID"
#define FIREBASE_CLIENT_EMAIL "CLIENT_EMAIL"
const char PRIVATE_KEY[] PROGMEM = "-----BEGIN PRIVATE KEY-----XXXXXXXXXXXX-----END PRIVATE KEY-----\n";
#define DATABASE_URL "URL"
#define DATABASE_SECRET "DATABASE_SECRET"
#include <FirebaseJson.h>
void authHandler();
void printResult(AsyncResult &aResult);
bool mofifyRules(const String &controlPath, const String &readCondition, const String &writeCondition, const String &databaseSecret);
void printError(int code, const String &msg);
void timeStatusCB(uint32_t &ts);
String genUUID();
DefaultNetwork network;
CustomAuth custom_auth(timeStatusCB, API_KEY, FIREBASE_CLIENT_EMAIL, FIREBASE_PROJECT_ID, PRIVATE_KEY, "peter" /* UID */, "" /* claims can be set later */, 3600 /* expire period in seconds (<3600) */);
FirebaseApp app;
#if defined(ESP32) || defined(ESP8266) || defined(ARDUINO_RASPBERRY_PI_PICO_W)
#include <WiFiClientSecure.h>
WiFiClientSecure ssl_client;
#elif defined(ARDUINO_ARCH_SAMD) || defined(ARDUINO_UNOWIFIR4) || defined(ARDUINO_GIGA) || defined(ARDUINO_OPTA) || defined(ARDUINO_PORTENTA_C33) || defined(ARDUINO_NANO_RP2040_CONNECT)
#include <WiFiSSLClient.h>
WiFiSSLClient ssl_client;
#endif
using AsyncClient = AsyncClientClass;
AsyncClient aClient(ssl_client, getNetwork(network));
RealtimeDatabase Database;
AsyncResult aResult_no_callback;
unsigned long ms = 0;
void setup()
{
Serial.begin(115200);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("Connecting to Wi-Fi");
while (WiFi.status() != WL_CONNECTED)
{
Serial.print(".");
delay(300);
}
Serial.println();
Serial.print("Connected with IP: ");
Serial.println(WiFi.localIP());
Serial.println();
Firebase.printf("Firebase Client v%s\n", FIREBASE_CLIENT_VERSION);
#if defined(ESP32) || defined(ESP8266) || defined(PICO_RP2040)
ssl_client.setInsecure();
#if defined(ESP8266)
ssl_client.setBufferSizes(4096, 1024);
#endif
#endif
aClient.setAsyncResult(aResult_no_callback);
// The following process will modify the Realtime Database security rules to allow the read/write accesses only at UsersData/$userId
// if the auth.token variables are match the custom claims we set.
//
// Here is the final security rules we want to set in this example.
//
// {
// "rules": {
// "logs": {
// "database": {
// "$resource": {
// "$group": {
// "$userId": {
// ".read": "($userId === auth.uid && auth.token.resource === $resource && auth.token.group === $group)",
// ".write": "($userId === auth.uid && auth.token.resource === $resource && auth.token.group === $group)"
// }
// }
// }
// }
// }
// }
// }
//
// We use $ variable in the rules e.g. $resource, $group, and $userId to capture the path segment that are used to compare with the auth variables
// that we set e.g. $userId will be compared with UID (auth.uid), $resource will be compared with resource claim (auth.token.resource),
// and $group will be compared with group claim (auth.token.group).
//
// For more information, visit https://firebase.google.com/docs/database/security/rules-conditions#using_variables_to_capture_path_segments
//
// =========
// IMPORTANT
// =========
// To allow read/write access only the conditions we set above,
// you have to remove the following rules from your security rules (if it exists).
// ".read": "auth != null"
// ".write": "auth != null"
// ".read": "true"
// ".write": "true"
// ".read": "some other conditions that allow access by date"
// ".write": "some other conditions that allow access by date"
String controlPath = "/logs/database/$resource/$group/$userId";
String readConditions = "($userId === auth.uid && auth.token.resource === $resource && auth.token.group === $group)";
String writeConditions = readConditions;
mofifyRules(controlPath, readConditions, writeConditions, DATABASE_SECRET);
// We will set the claims to the token we used here (ID token using CustomAuth).
// We set the values of the claims to math the auth.token varibles values in the security rules conditions.
// The claims string must be JSON serialized string when passing to CustomAuth::setClaims or CustomAuth class constructor.
// For more details about custom claims, please see https://firebase.google.com/docs/auth/admin/custom-claims.
// The resource claim value can be access via auth.token.resource in the security rules.
// And group claim value can be access via auth.token.group.
String claims = "{\"resource\":\"products\", \"group\":\"user\"}";
custom_auth.setClaims(claims);
// Now we authenticate (sign in) with CustomAuth (ID token with custom UID and claims).
Serial.println("Initializing the app...");
initializeApp(aClient, app, getAuth(custom_auth), aResult_no_callback);
authHandler();
app.getApp<RealtimeDatabase>(Database);
Database.url(DATABASE_URL);
}
void loop()
{
authHandler();
Database.loop();
if (app.ready() && (ms == 0 || millis() - ms > 10000))
{
ms = millis();
// From the UID, claims and security rules we have set,
// it only allows us to access at logs/database/products/user/peter/...
// Because the resource in the claim is products and group claim is user.
String path = "logs/database/products/user/";
path += app.getUid();
path += "/value";
Serial.print("Setting the int value to the granted path... ");
// This should be ok. Because we write to the path /UsersData/Node1/... which is allowed in the security rules.
bool status = Database.set<int>(aClient, path, 12345);
if (status)
Serial.println("ok");
else
printError(aClient.lastError().code(), aClient.lastError().message());
Serial.print("Setting the int value to outside of granted path... ");
// This should be failed because we write to the path that is not allowed.
// Only /UsersData/Node1/... is allowed in the security rules.
status = Database.set<int>(aClient, "/test/int", 12345);
if (status)
Serial.println("ok");
else
printError(aClient.lastError().code(), aClient.lastError().message());
// If you try to change the claim value e.g. from {"foo":"bar"} to {"foo":"bear"} the write and read accesses will be denied.
}
}
void authHandler()
{
// Blocking authentication handler with timeout
unsigned long ms = millis();
while (app.isInitialized() && !app.ready() && millis() - ms < 120 * 1000)
{
// The JWT token processor required for ServiceAuth and CustomAuth authentications.
// JWT is a static object of JWTClass and it's not thread safe.
// In multi-threaded operations (multi-FirebaseApp), you have to define JWTClass for each FirebaseApp,
// and set it to the FirebaseApp via FirebaseApp::setJWTProcessor(<JWTClass>), before calling initializeApp.
JWT.loop(app.getAuth());
printResult(aResult_no_callback);
}
}
void printResult(AsyncResult &aResult)
{
if (aResult.isEvent())
{
Firebase.printf("Event task: %s, msg: %s, code: %d\n", aResult.uid().c_str(), aResult.appEvent().message().c_str(), aResult.appEvent().code());
}
if (aResult.isDebug())
{
Firebase.printf("Debug task: %s, msg: %s\n", aResult.uid().c_str(), aResult.debug().c_str());
}
if (aResult.isError())
{
Firebase.printf("Error task: %s, msg: %s, code: %d\n", aResult.uid().c_str(), aResult.error().message().c_str(), aResult.error().code());
}
if (aResult.available())
{
if (aResult.to<RealtimeDatabaseResult>().name().length())
Firebase.printf("task: %s, name: %s\n", aResult.uid().c_str(), aResult.to<RealtimeDatabaseResult>().name().c_str());
Firebase.printf("task: %s, payload: %s\n", aResult.uid().c_str(), aResult.c_str());
}
}
void printError(int code, const String &msg)
{
Firebase.printf("Error, msg: %s, code: %d\n", msg.c_str(), code);
}
void timeStatusCB(uint32_t &ts)
{
#if defined(ESP8266) || defined(ESP32) || defined(CORE_ARDUINO_PICO)
if (time(nullptr) < FIREBASE_DEFAULT_TS)
{
configTime(3 * 3600, 0, "pool.ntp.org");
while (time(nullptr) < FIREBASE_DEFAULT_TS)
{
delay(100);
}
}
ts = time(nullptr);
#elif __has_include(<WiFiNINA.h>) || __has_include(<WiFi101.h>)
ts = WiFi.getTime();
#endif
}
/**
* @param controlPath The path that the which we want to control access.
* @param readCondition The read access condition.
* @param writeCondition The write access condition.
* @param databaseSecret The database secret.
*/
bool mofifyRules(const String &controlPath, const String &readCondition, const String &writeCondition, const String &databaseSecret)
{
// Use database secret for to allow security rules access.
// The ServiceAuth (OAuth2.0 access token authorization) can also be used
// but database secret is more simple for this case.
LegacyToken legacy_token(databaseSecret);
initializeApp(aClient, app, getAuth(legacy_token));
app.getApp<RealtimeDatabase>(Database);
Database.url(DATABASE_URL);
Serial.print("Getting the security rules to check the existing conditions... ");
String jsonStr = Database.get<String>(aClient, ".settings/rules");
// If security rules are ready get.
if (aClient.lastError().code() == 0)
{
Serial.println("ok");
bool ret = true;
FirebaseJsonData parseResult;
FirebaseJson currentRules(jsonStr);
bool readConditionExists = false, writeConditionExists = false;
String rulePath = "rules";
if (controlPath.length() && controlPath[0] != '/')
rulePath += '/';
rulePath += controlPath;
// Check the read condition exists or matches
if (readCondition.length() > 0)
{
String readPath = rulePath;
readPath += "/.read";
if (currentRules.get(parseResult, readPath.c_str()) && strcmp(parseResult.to<const char *>(), readCondition.c_str()) == 0)
readConditionExists = true;
}
// Check the write condition exists or matches
if (writeCondition.length() > 0)
{
String writePath = rulePath;
writePath += "/.write";
if (currentRules.get(parseResult, writePath.c_str()) && strcmp(parseResult.to<const char *>(), writeCondition.c_str()) == 0)
writeConditionExists = true;
}
// Add conditions if they do not exist.
if (!readConditionExists || !writeConditionExists)
{
FirebaseJson addedRules;
if (!readConditionExists)
addedRules.add(".read", readCondition);
if (!writeConditionExists)
addedRules.add(".write", writeCondition);
currentRules.set(rulePath, addedRules);
String modifiedRules;
currentRules.toString(modifiedRules, true);
Serial.print("Setting the security rules to add the modified conditions... ");
bool status = Database.set<object_t>(aClient, ".settings/rules", object_t(modifiedRules));
if (status)
Serial.println("ok");
else
printError(aClient.lastError().code(), aClient.lastError().message());
}
else
{
Serial.println("The rules exist, nothing to change");
}
currentRules.clear();
return aClient.lastError().code() == 0;
}
else
printError(aClient.lastError().code(), aClient.lastError().message());
Serial.println("-----------------------------------");
return false;
}