Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions dart/sign_in_with_google/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.dart_tool/
.packages
pubspec.lock
112 changes: 112 additions & 0 deletions dart/sign_in_with_google/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# sign-in-with-google

This function:

1. Verifies a Google ID token obtained from the client application.
1. If a user with matching id or email doesn't exist, a new user will be created.
1. The user's email will be verified if Google has verified it and it hasn't been already in Appwrite.
1. A token will be returned allowing the user to exchange the token for a session via `account.createSession()`.

> Note: This function verifies the Google ID token by validating its cryptographic signature using Google's public keys (JWK format), as recommended in the [official documentation](https://developers.google.com/identity/gsi/web/guides/verify-google-id-token). The function also validates the token's audience, issuer, and expiry claims.

## 🧰 Usage

### POST /

**Headers**

The Content-Type header must be set to `application/json` so that the request body can be properly parsed as JSON.

* `Content-Type`: `application/json`

**Request**

This function accepts:

* `idToken` (required) - Google ID token obtained from the client-side Google Sign-In flow

Sample request body:

```json
{
"idToken": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjdlMzA3..."
}
```

**Response**

This function returns:

* `secret` - `secret` to be passed to `account.createSession()` to create a session
* `userId` - `userId` to be passed to `account.createSession()` to create a session
* `expire` - ISO formatted timestamp for when the secret expires

Sample `200` Response:

```json
{
"secret": "0cbdd4fd7638e0f3f55871adf2256f8f42f6faa01c9300e482c9a585b76611343dee8562ce4421b1cf9e9de6f8341fb2286499cb7992d02accd2dc699211008c",
"userId": "112345678901234567890",
"expire": "2025-07-15T00:10:21.345+00:00"
}
```

## ⚙️ Configuration

| Setting | Value |
| ----------------- | --------------- |
| Runtime | Dart (3.5) |
| Entrypoint | `lib/main.dart` |
| Build Commands | `dart pub get` |
| Permissions | `any` |
| Timeout (Seconds) | 15 |
| Scopes | `users.read`, `users.write` |

## 🔒 Environment Variables

The following environment variables are required:

* `GOOGLE_CLIENT_ID` - Your Google OAuth 2.0 Client ID from the Google Cloud Console

## 📱 Client-Side Integration

To obtain the Google ID token from your client application, use the [google_sign_in](https://pub.dev/packages/google_sign_in) package:

```dart
import 'package:google_sign_in/google_sign_in.dart';

final GoogleSignIn _googleSignIn = GoogleSignIn(
clientId: 'YOUR_CLIENT_ID.apps.googleusercontent.com',
);

Future<void> signInWithGoogle() async {
try {
final GoogleSignInAccount? account = await _googleSignIn.signIn();
if (account == null) return;

final GoogleSignInAuthentication auth = await account.authentication;
final String? idToken = auth.idToken;

if (idToken != null) {
// Send the idToken to your Appwrite function
final response = await http.post(
Uri.parse('YOUR_FUNCTION_URL'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'idToken': idToken}),
);
// Handle the response
}
} catch (error) {
print('Error signing in with Google: $error');
}
}
```

## 🔐 Security Notes

* The Google ID token's cryptographic signature is verified using Google's public keys (RSA)
* The correct public key is selected using the `kid` (key ID) from the JWT header
* The token's audience (client ID) is verified to match your application
* The token's issuer is verified to be Google (`accounts.google.com`)
* The token's expiration time is checked to ensure it hasn't expired
* Email verification status from Google is honored in Appwrite
1 change: 1 addition & 0 deletions dart/sign_in_with_google/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include: package:lints/recommended.yaml
176 changes: 176 additions & 0 deletions dart/sign_in_with_google/lib/main.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:dart_appwrite/dart_appwrite.dart';
import 'package:dart_appwrite/models.dart';
import 'package:http/http.dart' as http;
import 'package:jose/jose.dart';

Future<dynamic> main(final context) async {
final requiredEnvVars = ['GOOGLE_CLIENT_ID'];
for (var varName in requiredEnvVars) {
if (Platform.environment[varName]?.isEmpty ?? true) {
throw Exception('Environment variable $varName must be set.');
}
}
Comment thread
ahmtydn marked this conversation as resolved.
Outdated

final googleClientId = Platform.environment['GOOGLE_CLIENT_ID']!;

final reqBody = context.req.bodyJson as Map<String, dynamic>;
final idToken = reqBody['idToken'] ?? '';

// Validate input
if (idToken.isEmpty) {
throw Exception('idToken must be provided in the request body.');
}

// Fetch Google's public keys for JWT verification
http.Response certsResponse;
try {
certsResponse = await http
.get(
Uri.parse('https://www.googleapis.com/oauth2/v3/certs'),
)
.timeout(
const Duration(seconds: 5),
onTimeout: () {
throw TimeoutException(
'Request to fetch Google public keys timed out after 5 seconds',
);
},
);
} on TimeoutException catch (e) {
context.log('Timeout fetching Google public keys: $e');
throw Exception(
'Failed to fetch Google public keys: Request timed out. Please try again.');
} catch (e) {
context.log('Error fetching Google public keys: $e');
throw Exception('Failed to fetch Google public keys: $e');
}
Comment thread
ahmtydn marked this conversation as resolved.
Outdated

if (certsResponse.statusCode != 200) {
throw Exception(
'Failed to fetch Google public keys: ${certsResponse.body}');
}

final jwks = JsonWebKeySet.fromJson(json.decode(certsResponse.body));

// Parse the JWT to get the header
final jwtParts = idToken.split('.');
if (jwtParts.length != 3) {
throw Exception('Invalid JWT format.');
}

// Decode header to get the key ID (kid)
final headerJson = json.decode(
utf8.decode(base64Url.decode(base64Url.normalize(jwtParts[0]))),
);
final keyId = headerJson['kid'] as String?;
if (keyId == null) {
throw Exception('JWT header does not contain a key ID (kid).');
}

// Find the matching key by kid (key ID)
final key = jwks.keys.firstWhere(
(k) => k.keyId == keyId,
orElse: () => throw Exception('No matching key found for kid: $keyId'),
);

// Create a key store and verify the signature
final keyStore = JsonWebKeyStore()..addKey(key);

JsonWebToken jwt;
try {
// Verify and decode the JWT signature
jwt = await JsonWebToken.decodeAndVerify(idToken, keyStore);
} catch (e) {
throw Exception('Failed to verify JWT signature: $e');
}

// Extract claims from the verified token
final claims = jwt.claims;

// Verify the token's audience matches our client ID
final audiences = claims.audience ?? [];
if (!audiences.contains(googleClientId)) {
throw Exception('ID Token audience does not match the expected client ID.');
}

// Verify the token is issued by Google
final iss = claims.issuer ?? '';
if (iss != 'https://accounts.google.com' && iss != 'accounts.google.com') {
throw Exception('ID Token is not issued by Google.');
}

// Verify the token has not expired
final exp = claims.expiry;
if (exp == null || exp.isBefore(DateTime.now())) {
throw Exception('ID Token has expired.');
}

Comment thread
ahmtydn marked this conversation as resolved.
// Extract user information
final userId = claims.subject ?? '';
if (userId.isEmpty) {
throw Exception('ID Token does not contain a valid subject (sub) claim.');
}

final claimsJson = claims.toJson();
final email = claimsJson['email']?.toString() ?? '';
final emailVerified = claimsJson['email_verified'] == true;
final name = claimsJson['name']?.toString() ?? '';

// You can use the Appwrite SDK to interact with other services
// For this example, we're using the Users service
final client = Client()
.setEndpoint(Platform.environment['APPWRITE_FUNCTION_API_ENDPOINT']!)
.setProject(Platform.environment['APPWRITE_FUNCTION_PROJECT_ID']!)
.setKey(context.req.headers['x-appwrite-key'] ?? '');
final users = Users(client);
Comment thread
ahmtydn marked this conversation as resolved.
Outdated

// Find user by ID
User? user;
try {
user = await users.get(userId: userId);
} on AppwriteException catch (e) {
if (e.type != 'user_not_found') {
rethrow;
}
}

// Find user by email
if (user == null && email.isNotEmpty) {
final userList = await users.list(queries: [Query.equal('email', email)]);
if (userList.users.isNotEmpty) {
user = userList.users.first;
}
}

// If user does not exist, create a new user
user ??= await users.create(
userId: ID.custom(userId),
email: email.isEmpty ? null : email,
name: name.isEmpty ? null : name,
);

// Mark the user as verified if the email is verified by Google and not already verified
if (emailVerified && !user.emailVerification) {
await users.updateEmailVerification(
userId: user.$id,
emailVerification: true,
);
}

// Create token
final token = await users.createToken(
userId: user.$id,
expire: 60,
length: 128,
);

return context.res.json({
'secret': token.secret,
'userId': user.$id,
'expire': token.expire,
});
}
13 changes: 13 additions & 0 deletions dart/sign_in_with_google/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: sign_in_with_google
version: 1.0.0

environment:
sdk: '>=3.5.0 <4.0.0'

dependencies:
dart_appwrite: ^19.4.0
http: ^1.6.0
jose: ^0.3.5
Comment thread
ahmtydn marked this conversation as resolved.
Outdated
Comment thread
ahmtydn marked this conversation as resolved.
Outdated

dev_dependencies:
lints: ^2.0.0