Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
30 changes: 30 additions & 0 deletions dart/sign_in_with_apple/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# See https://www.dartlang.org/guides/libraries/private-files

# Files and directories created by pub
.dart_tool/
.packages
build/
# If you're building an application, you may want to check-in your pubspec.lock
pubspec.lock

# Directory created by dartdoc
# If you don't generate documentation locally you can remove this line.
doc/api/

# dotenv environment variables file
.env*

# Avoid committing generated Javascript files:
*.dart.js
*.info.json # Produced by the --dump-info flag.
*.js # When generated by dart2js. Don't specify *.js if your
# project includes source files written in JavaScript.
*.js_
*.js.deps
*.js.map

.flutter-plugins
.flutter-plugins-dependencies

# Directory used by Appwrite CLI for local development
.appwrite
76 changes: 76 additions & 0 deletions dart/sign_in_with_apple/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# sign-in-with-apple

This function:

1. Exchanges an authorization code with Apple to obtain the user's id token.
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 is hasn't been already.
1. A token will be returned allowing the user to exchange the token for a session via `account.createSession()`.

> Note: this function uses an md5 hash of the `sub` as the user's id since the value from Apple is too long and has unsupported characters.

## 🧰 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:

* `code` (required) - authorization code from the Sign in with Apple credential
* `firstName` - given name from the Sign in with Apple credential
* `lastName` - family name from the Sign in with Apple credential

Sample request body:

```json
{
"code": "c361a519253b3486ea3c7ecd4e9b6903f.0.suut.3LCHm9ytku1B2v4r5IayPQ",
"firstName": "Walter",
"lastName": "O'Brien",
}
```

**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": "90a5450f396c242637c39b4c39e07af4",
"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:

* `BUNDLE_ID` - the bundle Id of the app that generated the authorization code
* `TEAM_ID` - Apple Developer team Id
* `KEY_ID` - Id of the key from the Apple Developer portal
* `KEY_CONTENTS_ENCODED` - base64 encoded p8 certificate
1 change: 1 addition & 0 deletions dart/sign_in_with_apple/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include: package:lints/recommended.yaml
145 changes: 145 additions & 0 deletions dart/sign_in_with_apple/lib/main.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:crypto/crypto.dart';
import 'package:dart_appwrite/dart_appwrite.dart';
import 'package:dart_appwrite/models.dart';
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
import 'package:http/http.dart' as http;

Future<dynamic> main(final context) async {
final requiredEnvVars = [
'BUNDLE_ID',
'TEAM_ID',
'KEY_ID',
'KEY_CONTENTS_ENCODED'
];
for (var varName in requiredEnvVars) {
if (Platform.environment[varName]?.isEmpty ?? true) {
throw Exception('Environment variable $varName must be set.');
}
}

final bundleId = Platform.environment['BUNDLE_ID']!;
final teamId = Platform.environment['TEAM_ID']!;
final keyId = Platform.environment['KEY_ID']!;
final keyContentsEncoded = Platform.environment['KEY_CONTENTS_ENCODED']!;
final keyContents = utf8.decode(base64Decode(keyContentsEncoded));

final key = ECPrivateKey(keyContents);

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

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

// Create a JWT client secret
final header = {'alg': 'ES256', 'kid': keyId};
final jwt = JWT(
{},
header: header,
subject: bundleId,
issuer: teamId,
audience: Audience.one('https://appleid.apple.com'),
);
final clientSecret = jwt.sign(
key,
algorithm: JWTAlgorithm.ES256,
expiresIn: Duration(minutes: 5),
);

final authTokenRequestBody = {
'grant_type': 'authorization_code',
'code': code,
'client_id': bundleId,
'client_secret': clientSecret,
};

final authTokenResponse = await http.post(
Uri.parse('https://appleid.apple.com/auth/token'),
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: authTokenRequestBody,
);

if (authTokenResponse.statusCode != 200) {
throw Exception(
'Failed to exchange code for token: ${authTokenResponse.body}');
}

final body = json.decode(authTokenResponse.body);

// Use access token to fetch any additional information if needed
// final accessToken = body['access_token'] ?? '';

// Store refresh token if you want to refresh the access token later
// final refreshToken = body['refresh_token'] ?? '';

final idToken = JWT.decode(body['id_token']);
final sub = idToken.payload['sub'] ?? '';
if (sub.isEmpty) {
throw Exception('ID Token does not contain a valid subject (sub) claim.');
}
// Hash the sub because it is too long and has characters that are not allowed in Appwrite user IDs
final userId = md5.convert(utf8.encode(sub)).toString();
final email = idToken.payload['email'] ?? '';
final userName = '$firstName $lastName'.trim();

// 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
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,
name: userName.isEmpty ? null : userName,
);

// Mark the user as verified if not already verified
if (!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,
});
}
14 changes: 14 additions & 0 deletions dart/sign_in_with_apple/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: sign_in_with_apple
version: 1.0.0

environment:
sdk: ^2.17.0

dependencies:
dart_appwrite: ^16.0.0
dart_jsonwebtoken: ^3.2.0
http: ^1.4.0
crypto: ^3.0.6

dev_dependencies:
lints: ^2.0.0