Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
114 changes: 114 additions & 0 deletions dart/sign_in_with_google/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
````markdown
# 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 using Google's tokeninfo endpoint as described in the [official documentation](https://developers.google.com/identity/gsi/web/guides/verify-google-id-token).

## 🧰 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 is verified using Google's official tokeninfo endpoint
* The token's audience (client ID) is verified to match your application
* The token's issuer is verified to be Google
* The token's expiration time is checked
* 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
120 changes: 120 additions & 0 deletions dart/sign_in_with_google/lib/main.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
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;

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.');
}
}

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.');
}

// Verify the Google ID token
final tokenInfoResponse = await http.get(
Uri.parse('https://oauth2.googleapis.com/tokeninfo?id_token=$idToken'),
);

if (tokenInfoResponse.statusCode != 200) {
throw Exception(
'Failed to verify Google ID token: ${tokenInfoResponse.body}',
);
}

final tokenInfo = json.decode(tokenInfoResponse.body);

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

// Verify the token is issued by Google
final iss = tokenInfo['iss'] ?? '';
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 = int.tryParse(tokenInfo['exp']?.toString() ?? '0') ?? 0;
final currentTime = DateTime.now().millisecondsSinceEpoch ~/ 1000;
if (exp <= currentTime) {
throw Exception('ID Token has expired.');
}

// Extract user information
final userId = tokenInfo['sub'] ?? '';
if (userId.isEmpty) {
throw Exception('ID Token does not contain a valid subject (sub) claim.');
}

final email = tokenInfo['email'] ?? '';
final emailVerified = tokenInfo['email_verified'] == 'true';
final name = tokenInfo['name'] ?? '';
final picture = tokenInfo['picture'] ?? '';

// 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);

// 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) {
users.updateEmailVerification(userId: userId, 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,
});
}
12 changes: 12 additions & 0 deletions dart/sign_in_with_google/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
name: sign_in_with_google
version: 1.0.0

environment:
sdk: ^2.17.0

dependencies:
dart_appwrite: ^16.0.0
http: ^1.4.0

dev_dependencies:
lints: ^2.0.0