Skip to content

Commit a284cdd

Browse files
authored
feat: add identity triggers (#9)
1 parent fe7c739 commit a284cdd

File tree

12 files changed

+2471
-3
lines changed

12 files changed

+2471
-3
lines changed

example/basic/lib/main.dart

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,100 @@ void main(List<String> args) {
317317
},
318318
);
319319

320+
// ==========================================================================
321+
// Identity Platform (Auth Blocking) trigger examples
322+
// ==========================================================================
323+
324+
// Before user created - runs before a new user is created
325+
firebase.identity.beforeUserCreated(
326+
options: const BlockingOptions(
327+
idToken: true,
328+
accessToken: true,
329+
),
330+
(AuthBlockingEvent event) async {
331+
final user = event.data;
332+
print('Before user created:');
333+
print(' UID: ${user?.uid}');
334+
print(' Email: ${user?.email}');
335+
print(' Provider: ${event.additionalUserInfo?.providerId}');
336+
337+
// Example: Block users with certain email domains
338+
final email = user?.email;
339+
if (email != null && email.endsWith('@blocked.com')) {
340+
throw PermissionDeniedError('Email domain not allowed');
341+
}
342+
343+
// Example: Set custom claims based on email domain
344+
if (email != null && email.endsWith('@admin.com')) {
345+
return const BeforeCreateResponse(
346+
customClaims: {'admin': true},
347+
);
348+
}
349+
350+
return null;
351+
},
352+
);
353+
354+
// Before user signed in - runs before a user signs in
355+
firebase.identity.beforeUserSignedIn(
356+
options: const BlockingOptions(
357+
idToken: true,
358+
),
359+
(AuthBlockingEvent event) async {
360+
final user = event.data;
361+
print('Before user signed in:');
362+
print(' UID: ${user?.uid}');
363+
print(' Email: ${user?.email}');
364+
print(' IP Address: ${event.ipAddress}');
365+
366+
// Example: Add session claims for tracking
367+
return BeforeSignInResponse(
368+
sessionClaims: {
369+
'lastLogin': DateTime.now().toIso8601String(),
370+
'signInIp': event.ipAddress,
371+
},
372+
);
373+
},
374+
);
375+
376+
// Before email sent - runs before password reset or sign-in emails
377+
firebase.identity.beforeEmailSent(
378+
(AuthBlockingEvent event) async {
379+
print('Before email sent:');
380+
print(' Email Type: ${event.emailType?.value}');
381+
print(' IP Address: ${event.ipAddress}');
382+
383+
// Example: Rate limit password reset emails
384+
// In production, you'd check against a database
385+
if (event.emailType == EmailType.passwordReset) {
386+
// Could return BeforeEmailResponse(
387+
// recaptchaActionOverride: RecaptchaActionOptions.block,
388+
// ) to block suspicious requests
389+
}
390+
391+
return null;
392+
},
393+
);
394+
395+
// Before SMS sent - runs before MFA or sign-in SMS messages
396+
firebase.identity.beforeSmsSent(
397+
(AuthBlockingEvent event) async {
398+
print('Before SMS sent:');
399+
print(' SMS Type: ${event.smsType?.value}');
400+
print(' Phone: ${event.additionalUserInfo?.phoneNumber}');
401+
402+
// Example: Block SMS to certain country codes
403+
final phone = event.additionalUserInfo?.phoneNumber;
404+
if (phone != null && phone.startsWith('+1900')) {
405+
return const BeforeSmsResponse(
406+
recaptchaActionOverride: RecaptchaActionOptions.block,
407+
);
408+
}
409+
410+
return null;
411+
},
412+
);
413+
320414
print('Functions registered successfully!');
321415
});
322416
}

lib/builder.dart

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ class _TypeCheckers {
3636
TypeChecker.fromRuntime(ff.AppDistributionNamespace);
3737
static final performanceNamespace =
3838
TypeChecker.fromRuntime(ff.PerformanceNamespace);
39+
static final identityNamespace =
40+
TypeChecker.fromRuntime(ff.IdentityNamespace);
3941
}
4042

4143
/// The main builder that generates functions.yaml.
@@ -179,6 +181,11 @@ class _FirebaseFunctionsVisitor extends RecursiveAstVisitor<void> {
179181
_extractPerformanceAlertFunction(node, methodName);
180182
}
181183

184+
// Check for Identity function declarations
185+
if (target != null && _isIdentityNamespace(target)) {
186+
_extractIdentityFunction(node, methodName);
187+
}
188+
182189
// Check for parameter definitions (top-level function calls with no target)
183190
if (target == null && _isParamDefinition(methodName)) {
184191
_extractParameterFromMethod(node, methodName);
@@ -325,6 +332,13 @@ class _FirebaseFunctionsVisitor extends RecursiveAstVisitor<void> {
325332
return _TypeCheckers.performanceNamespace.isExactlyType(staticType);
326333
}
327334

335+
/// Checks if the target is firebase.identity.
336+
bool _isIdentityNamespace(Expression target) {
337+
final staticType = target.staticType;
338+
if (staticType == null) return false;
339+
return _TypeCheckers.identityNamespace.isExactlyType(staticType);
340+
}
341+
328342
/// Checks if this is a parameter definition function.
329343
bool _isParamDefinition(String name) =>
330344
name == 'defineString' ||
@@ -585,6 +599,60 @@ class _FirebaseFunctionsVisitor extends RecursiveAstVisitor<void> {
585599
);
586600
}
587601

602+
/// Extracts an Identity function declaration.
603+
void _extractIdentityFunction(MethodInvocation node, String methodName) {
604+
// Map method names to event types
605+
final eventType = switch (methodName) {
606+
'beforeUserCreated' => 'beforeCreate',
607+
'beforeUserSignedIn' => 'beforeSignIn',
608+
'beforeEmailSent' => 'beforeSendEmail',
609+
'beforeSmsSent' => 'beforeSendSms',
610+
_ => null,
611+
};
612+
613+
if (eventType == null) return;
614+
615+
// Extract options if present
616+
final optionsArg = _findNamedArg(node, 'options');
617+
bool? idToken;
618+
bool? accessToken;
619+
bool? refreshToken;
620+
621+
if (optionsArg is InstanceCreationExpression) {
622+
idToken = _extractBoolField(optionsArg, 'idToken');
623+
accessToken = _extractBoolField(optionsArg, 'accessToken');
624+
refreshToken = _extractBoolField(optionsArg, 'refreshToken');
625+
}
626+
627+
// Function name is the event type
628+
final functionName = eventType;
629+
630+
endpoints[functionName] = _EndpointSpec(
631+
name: functionName,
632+
type: 'blocking',
633+
blockingEventType: eventType,
634+
idToken: idToken,
635+
accessToken: accessToken,
636+
refreshToken: refreshToken,
637+
options: optionsArg is InstanceCreationExpression ? optionsArg : null,
638+
variableToParamName: _variableToParamName,
639+
);
640+
}
641+
642+
/// Extracts a boolean field from an InstanceCreationExpression.
643+
bool? _extractBoolField(InstanceCreationExpression node, String fieldName) {
644+
final arg = node.argumentList.arguments
645+
.whereType<NamedExpression>()
646+
.where((e) => e.name.label.name == fieldName)
647+
.map((e) => e.expression)
648+
.firstOrNull;
649+
650+
if (arg is BooleanLiteral) {
651+
return arg.value;
652+
}
653+
return null;
654+
}
655+
588656
/// Extracts alert type value from an expression.
589657
String? _extractAlertTypeValue(Expression expression) {
590658
if (expression is InstanceCreationExpression) {
@@ -816,11 +884,15 @@ class _EndpointSpec {
816884
this.instance,
817885
this.alertType,
818886
this.appId,
887+
this.blockingEventType,
888+
this.idToken,
889+
this.accessToken,
890+
this.refreshToken,
819891
this.options,
820892
this.variableToParamName = const {},
821893
});
822894
final String name;
823-
// 'https', 'callable', 'pubsub', 'firestore', 'database', 'alert'
895+
// 'https', 'callable', 'pubsub', 'firestore', 'database', 'alert', 'blocking'
824896
final String type;
825897
final String? topic; // For Pub/Sub functions
826898
final String? firestoreEventType; // For Firestore: onDocumentCreated, etc.
@@ -832,6 +904,10 @@ class _EndpointSpec {
832904
final String? instance; // For Database: database instance or '*'
833905
final String? alertType; // For Alerts: crashlytics.newFatalIssue, etc.
834906
final String? appId; // For Alerts: optional app ID filter
907+
final String? blockingEventType; // For Identity: beforeCreate, etc.
908+
final bool? idToken; // For Identity: pass ID token
909+
final bool? accessToken; // For Identity: pass access token
910+
final bool? refreshToken; // For Identity: pass refresh token
835911
final InstanceCreationExpression? options;
836912
final Map<String, String> variableToParamName;
837913

@@ -1375,6 +1451,13 @@ String _generateYaml(
13751451
buffer.writeln('requiredAPIs:');
13761452
buffer.writeln(' - api: "cloudfunctions.googleapis.com"');
13771453
buffer.writeln(' reason: "Required for Cloud Functions"');
1454+
// Add identitytoolkit API if there are blocking functions
1455+
final hasBlockingFunctions =
1456+
endpoints.values.any((e) => e.type == 'blocking');
1457+
if (hasBlockingFunctions) {
1458+
buffer.writeln(' - api: "identitytoolkit.googleapis.com"');
1459+
buffer.writeln(' reason: "Needed for auth blocking functions"');
1460+
}
13781461
buffer.writeln();
13791462

13801463
// Generate endpoints section
@@ -1578,6 +1661,30 @@ String _generateYaml(
15781661
buffer.writeln(' appid: "${endpoint.appId}"');
15791662
}
15801663
buffer.writeln(' retry: false');
1664+
} else if (endpoint.type == 'blocking' &&
1665+
endpoint.blockingEventType != null) {
1666+
buffer.writeln(' blockingTrigger:');
1667+
buffer.writeln(
1668+
' eventType: "providers/cloud.auth/eventTypes/user.${endpoint.blockingEventType}"',
1669+
);
1670+
1671+
// Only include token options for beforeCreate and beforeSignIn
1672+
final isAuthEvent = endpoint.blockingEventType == 'beforeCreate' ||
1673+
endpoint.blockingEventType == 'beforeSignIn';
1674+
if (isAuthEvent) {
1675+
buffer.writeln(' options:');
1676+
if (endpoint.idToken ?? false) {
1677+
buffer.writeln(' idToken: true');
1678+
}
1679+
if (endpoint.accessToken ?? false) {
1680+
buffer.writeln(' accessToken: true');
1681+
}
1682+
if (endpoint.refreshToken ?? false) {
1683+
buffer.writeln(' refreshToken: true');
1684+
}
1685+
} else {
1686+
buffer.writeln(' options: {}');
1687+
}
15811688
}
15821689
}
15831690
}

lib/firebase_functions.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ export 'src/firebase.dart' show Firebase;
7777
export 'src/firestore/firestore.dart';
7878
// HTTPS triggers
7979
export 'src/https/https.dart';
80+
// Identity triggers
81+
export 'src/identity/identity.dart';
8082
// Pub/Sub triggers
8183
export 'src/pubsub/pubsub.dart';
8284
// Core runtime

lib/src/firebase.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'alerts/alerts_namespace.dart';
99
import 'database/database_namespace.dart';
1010
import 'firestore/firestore_namespace.dart';
1111
import 'https/https_namespace.dart';
12+
import 'identity/identity_namespace.dart';
1213
import 'pubsub/pubsub_namespace.dart';
1314

1415
/// Main Firebase Functions instance.
@@ -81,6 +82,9 @@ class Firebase {
8182

8283
/// Firebase Alerts namespace.
8384
AlertsNamespace get alerts => AlertsNamespace(this);
85+
86+
/// Identity Platform namespace.
87+
IdentityNamespace get identity => IdentityNamespace(this);
8488
}
8589

8690
/// Extension for internal function registration.

0 commit comments

Comments
 (0)