diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9463be1e0..7ca65423c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -306,7 +306,8 @@ jobs: # This will be a no-op under normal circumstances since the cluster would have been deployed # in deploy-cluster. It is needed in case we want to re-run the job after the cluster has been reaped. - - uses: realm/ci-actions/mdb-realm/deployApps@3f810b2d04e9dada2bde0b33ec90102e52a0b30a + - name: Create cluster + uses: realm/ci-actions/mdb-realm/deployApps@3f810b2d04e9dada2bde0b33ec90102e52a0b30a with: realmUrl: ${{ env.BAAS_URL }} atlasUrl: ${{ secrets.ATLAS_QA_URL }} @@ -376,7 +377,8 @@ jobs: # This will be a no-op under normal circumstances since the cluster would have been deployed # in deploy-cluster. It is needed in case we want to re-run the job after the cluster has been reaped. - - uses: realm/ci-actions/mdb-realm/deployApps@3f810b2d04e9dada2bde0b33ec90102e52a0b30a + - name: Create cluster + uses: realm/ci-actions/mdb-realm/deployApps@3f810b2d04e9dada2bde0b33ec90102e52a0b30a with: realmUrl: ${{ env.BAAS_URL }} atlasUrl: ${{ secrets.ATLAS_QA_URL }} diff --git a/.github/workflows/dart-desktop-tests.yml b/.github/workflows/dart-desktop-tests.yml index a48e32cb4..665b0c382 100644 --- a/.github/workflows/dart-desktop-tests.yml +++ b/.github/workflows/dart-desktop-tests.yml @@ -61,7 +61,8 @@ jobs: # This will be a no-op under normal circumstances since the cluster would have been deployed # in deploy-cluster. It is needed in case we want to re-run the job after the cluster has been reaped. - - uses: realm/ci-actions/mdb-realm/deployApps@3f810b2d04e9dada2bde0b33ec90102e52a0b30a + - name: Create cluster + uses: realm/ci-actions/mdb-realm/deployApps@3f810b2d04e9dada2bde0b33ec90102e52a0b30a with: realmUrl: ${{ env.BAAS_URL }} atlasUrl: ${{ secrets.ATLAS_QA_URL }} diff --git a/.github/workflows/flutter-desktop-tests.yml b/.github/workflows/flutter-desktop-tests.yml index 4577c29bd..b8e4880bd 100644 --- a/.github/workflows/flutter-desktop-tests.yml +++ b/.github/workflows/flutter-desktop-tests.yml @@ -70,7 +70,8 @@ jobs: # This will be a no-op under normal circumstances since the cluster would have been deployed # in deploy-cluster. It is needed in case we want to re-run the job after the cluster has been reaped. - - uses: realm/ci-actions/mdb-realm/deployApps@3f810b2d04e9dada2bde0b33ec90102e52a0b30a + - name: Create cluster + uses: realm/ci-actions/mdb-realm/deployApps@3f810b2d04e9dada2bde0b33ec90102e52a0b30a with: realmUrl: ${{ env.BAAS_URL }} atlasUrl: ${{ secrets.ATLAS_QA_URL }} diff --git a/.pubignore b/.pubignore index e3c1bdad1..6471e7d41 100644 --- a/.pubignore +++ b/.pubignore @@ -38,3 +38,6 @@ src/realm-core/**/*.pem # Ignore realm-core doc src/realm-core/doc +# Ignore test resources +test/data + diff --git a/CHANGELOG.md b/CHANGELOG.md index 826f4d6d7..79d2c6b92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ * Support `App.deleteUser ` for deleting user accounts. ([#679](https://github.com/realm/realm-dart/pull/679)) * Support Apple, Facebook and Google authentication. ([#740](https://github.com/realm/realm-dart/pull/740)) * Allow multiple anonymous sessions. When using anonymous authentication you can now easily log in with a different anonymous user than last time. ([#750](https://github.com/realm/realm-dart/pull/750)). +* Support `Credentials.jwt` for login user with JWT issued by custom provider . ([#715](https://github.com/realm/realm-dart/pull/715)) ### Internal * Added a command to `realm_dart` for deleting Atlas App Services applications. Usage: `dart run realm_dart delete-apps`. By default it will delete apps from `http://localhost:9090` which is the endpoint of the local docker image. If `--atlas-cluster` is provided, it will authenticate, delete the application from the provided cluster. (PR [#663](https://github.com/realm/realm-dart/pull/663)) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 81f4f013f..48c476ca4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,3 +26,74 @@ We love contributions to Realm! If you'd like to contribute code, documentation, Realm welcomes all contributions! The only requirement we have is that, like many other projects, we need to have a [Contributor License Agreement](https://en.wikipedia.org/wiki/Contributor_License_Agreement) (CLA) in place before we can accept any external code. Our own CLA is a modified version of the Apache Software Foundation’s CLA. [Please submit your CLA electronically using our Google form](https://docs.google.com/forms/d/1ga5zIS9qnwwFPmbq-orSPsiBIXQjltKg7ytHd2NmDYo/viewform) so we can accept your submissions. The GitHub username you file there will need to match that of your Pull Requests. If you have any questions or cannot file the CLA electronically, you can email . + +## Building the source + +### Building Realm Flutter + +* Clone the repo + ``` + git clone https://github.com/realm/realm-dart + git submodule update --init --recursive + ``` + +#### Build Realm Flutter native binaries + +* Android + ```bash + ./scripts/build-android.sh all + scripts\build-android.bat all + # Or for Android Emulator only + ./scripts/build-android.sh x86 + scripts\build-android.bat x86 + ``` + +* iOS + ```bash + ./scripts/build-ios.sh + # Or for iOS Simulator only + ./scripts/build-ios.sh simulator + ``` + +* Windows + ``` + scripts\build.bat + ``` +* MacOS + ``` + ./scripts/build-macos.sh + ``` + +* Linux + ``` + ./scripts/build-linux.sh + ``` + +### Building Realm Dart + +* Windows + ``` + scripts\build.bat + ``` +* MacOS + ``` + ./scripts/build-macos.sh + ``` +* Linux + ``` + ./scripts/build-linux.sh + ``` + +## Running tests + +See [test/README.md](https://github.com/realm/realm-dart/blob/master/test/README.md) for instructions on running tests. + +## Versioning + +Realm Flutter and Dart SDK packages follow [Semantic Versioning](https://semver.org/). +During the initial development the packages will be versioned according the scheme `0.major.minor+release stage` until the first stable version is reached then packages will be versioned with `major.minor.patch` scheme. + +The first versions will follow `0.1.0+preview`, `0.1.1+preview` etc. +Then next release stages will pick up the next minor version `0.1.2+beta`, `0.1.3+beta`. This will ensure dependencies are updated on `dart pub get` with the new `alpha`, `beta` versions. +If an `alpha` version is released before `beta` and it needs to not be considered for `pub get` then it should be marked as `prerelease` with `-alpha` so `0.1.2-alpha` etc. +Updating the major version with every release stage is also possible - `0.2.0+alpha`, `0.3.0+beta`, `0.3.1+beta`. diff --git a/README.md b/README.md index f7067c1ec..9e051f0e1 100644 --- a/README.md +++ b/README.md @@ -310,76 +310,12 @@ The Realm Dart package is `realm_dart` # Building the source -## Building Realm Flutter +See [CONTRIBUTING.md](https://github.com/realm/realm-dart/blob/master/CONTRIBUTING.md#building-the-source) for instructions about building the source. -* Clone the repo - ``` - git clone https://github.com/realm/realm-dart - git submodule update --init --recursive - ``` - -### Build Realm Flutter native binaries - -* Android - ```bash - ./scripts/build-android.sh all - scripts\build-android.bat all - # Or for Android Emulator only - ./scripts/build-android.sh x86 - scripts\build-android.bat x86 - ``` - -* iOS - ```bash - ./scripts/build-ios.sh - # Or for iOS Simulator only - ./scripts/build-ios.sh simulator - ``` - -* Windows - ``` - scripts\build.bat - ``` -* MacOS - ``` - ./scripts/build-macos.sh - ``` - -* Linux - ``` - ./scripts/build-linux.sh - ``` - -## Building Realm Dart - -* Windows - ``` - scripts\build.bat - ``` -* MacOS - ``` - ./scripts/build-macos.sh - ``` -* Linux - ``` - ./scripts/build-linux.sh - ``` - -## Running tests +# Running tests See [test/README.md](https://github.com/realm/realm-dart/blob/master/test/README.md) for instructions on running tests. -## Versioning - -Realm Flutter and Dart SDK packages follow [Semantic Versioning](https://semver.org/). -During the initial development the packages will be versioned according the scheme `0.major.minor+release stage` until the first stable version is reached then packages will be versioned with `major.minor.patch` scheme. - -The first versions will follow `0.1.0+preview`, `0.1.1+preview` etc. -Then next release stages will pick up the next minor version `0.1.2+beta`, `0.1.3+beta`. This will ensure dependencies are updated on `dart pub get` with the new `alpha`, `beta` versions. -If an `alpha` version is released before `beta` and it needs to not be considered for `pub get` then it should be marked as `prerelease` with `-alpha` so `0.1.2-alpha` etc. -Updating the major version with every release stage is also possible - `0.2.0+alpha`, `0.3.0+beta`, `0.3.1+beta`. - - # Code of Conduct This project adheres to the [MongoDB Code of Conduct](https://www.mongodb.com/community-code-of-conduct). diff --git a/lib/src/cli/atlas_apps/baas_client.dart b/lib/src/cli/atlas_apps/baas_client.dart index fc2860cbd..e1b1a3c0f 100644 --- a/lib/src/cli/atlas_apps/baas_client.dart +++ b/lib/src/cli/atlas_apps/baas_client.dart @@ -63,6 +63,7 @@ class BaasClient { late final String _appSuffix = '-${shortenDifferentiator(_differentiator)}-$_clusterName'; late String _groupId; + late String publicRSAKey = ''; BaasClient._(String baseUrl, String? differentiator, [this._clusterName]) : _baseUrl = '$baseUrl/api/admin/v3.0', @@ -166,7 +167,7 @@ class BaasClient { final resetFuncId = await _createFunction(app, 'resetFunc', _resetFuncSource); await enableProvider(app, 'anon-user'); - await enableProvider(app, 'local-userpass', '''{ + await enableProvider(app, 'local-userpass', config: '''{ "autoConfirm": ${(confirmationType == "auto").toString()}, "confirmEmailSubject": "Confirmation required", "confirmationFunctionName": "confirmFunc", @@ -180,6 +181,63 @@ class BaasClient { "runResetFunction": true }'''); + if (publicRSAKey.isNotEmpty) { + String publicRSAKeyEncoded = jsonEncode(publicRSAKey); + final dynamic createSecretResult = await _post('groups/$_groupId/apps/$appId/secrets', '{"name":"rsPublicKey","value":$publicRSAKeyEncoded}'); + String keyName = createSecretResult['name'] as String; + + await enableProvider(app, 'custom-token', config: '''{ + "audience": "mongodb.com", + "signingAlgorithm": "RS256", + "useJWKURI": false + }''', secretConfig: '''{ + "signingKeys": ["$keyName"] + }''', metadataFelds: '''{ + "required": false, + "name": "name.firstName", + "field_name": "firstName" + }, + { + "required": false, + "name": "name.lastName", + "field_name": "lastName" + }, + { + "required": true, + "name": "email", + "field_name": "name" + }, + { + "required": true, + "name": "email", + "field_name": "email" + }, + { + "required": false, + "name": "gender", + "field_name": "gender" + }, + { + "required": false, + "name": "birthDay", + "field_name": "birthDay" + }, + { + "required": false, + "name": "minAge", + "field_name": "minAge" + }, + { + "required": false, + "name": "maxAge", + "field_name": "maxAge" + }, + { + "required": false, + "name": "company", + "field_name": "company" + }'''); + } print('Creating database db_$name$_appSuffix'); await _createMongoDBService(app, '''{ @@ -209,7 +267,7 @@ class BaasClient { return app; } - Future enableProvider(BaasApp app, String type, [String config = '{}']) async { + Future enableProvider(BaasApp app, String type, {String config = '{}', String secretConfig = '{}', String metadataFelds = '{}'}) async { print('Enabling provider $type for ${app.clientAppId}'); final url = 'groups/$_groupId/apps/$app/auth_providers'; @@ -223,7 +281,9 @@ class BaasClient { "name": "$type", "type": "$type", "disabled": false, - "config": $config + "config": $config, + "secret_config": $secretConfig, + "metadata_fields": [$metadataFelds] }'''); } } diff --git a/lib/src/credentials.dart b/lib/src/credentials.dart index 4fdb508eb..5f0ede903 100644 --- a/lib/src/credentials.dart +++ b/lib/src/credentials.dart @@ -40,7 +40,8 @@ enum AuthProviderType { /// Authenticate with Google account google, - _custom, + /// For authenticating with JSON web token. + jwt, /// For authenticating with an email and a password. emailPassword, @@ -76,6 +77,12 @@ class Credentials { : _handle = realmCore.createAppCredentialsEmailPassword(email, password), provider = AuthProviderType.emailPassword; + /// Returns a [Credentials] object that can be used to authenticate a user with a custom JWT. + /// [Custom-JWT Authentication Docs](https://docs.mongodb.com/realm/authentication/custom-jwt) + Credentials.jwt(String token) + : _handle = realmCore.createAppCredentialsJwt(token), + provider = AuthProviderType.jwt; + /// Returns a [Credentials] object that can be used to authenticate a user with a Facebook account. Credentials.facebook(String accessToken) : _handle = realmCore.createAppCredentialsFacebook(accessToken), diff --git a/lib/src/native/realm_core.dart b/lib/src/native/realm_core.dart index 6cf72cf70..e27667a9d 100644 --- a/lib/src/native/realm_core.dart +++ b/lib/src/native/realm_core.dart @@ -931,6 +931,13 @@ class _RealmCore { }); } + RealmAppCredentialsHandle createAppCredentialsJwt(String token) { + return using((arena) { + final tokenPtr = token.toCharPtr(arena); + return RealmAppCredentialsHandle._(_realmLib.realm_app_credentials_new_jwt(tokenPtr)); + }); + } + RealmAppCredentialsHandle createAppCredentialsApple(String idToken) { return using((arena) { final idTokenPtr = idToken.toCharPtr(arena); diff --git a/test/README.md b/test/README.md index 853bc282f..c9aff530b 100644 --- a/test/README.md +++ b/test/README.md @@ -131,3 +131,59 @@ do day-to-day development as it allows you to get into a clean state with a sing ``` 7. Now you can run `dart test` and it should include the integration tests. + +## Generating tokens for testing "Custom JWT Authentication" + +The tests that requires these steps to be done are related to login into Atlas App using [Custom JWT Authentication](https://www.mongodb.com/docs/atlas/app-services/authentication/custom-jwt/) with JWT token issued by a custom token provider. In case you have already JWT generated from an external token provider you don't need to follow these steps. + +### How to generate custom JWT + +The custom tokens are using RSA algorithm and key pair of private (private_key.pem) and public (public_key.pem) keys. +For the purpose of the tests the token could be generated by the following procedure: + +1. Generate private_key.pem and public_key.pem files: + + Both files (private_key.pem and public public_key.pem) are generated with `openssl` command https://www.openssl.org/ + + * Download `openssl.exe` + + * Generate private key for singing tokens with command: `openssl genrsa -out private_key.pem 2048` + + * Generate public key for validating tokens with command: `openssl rsa -in private_key.pem -pubout -out public_key.pem` + + * For testing verification method [Manually Specify Signing Keys](https://www.mongodb.com/docs/atlas/app-services/authentication/custom-jwt/#manually-specify-signing-keys) public key has to be configured into Custom Authentication provider in the clout app. It is done automatically by baas_client.dart where the value of the public key is taken from constant `publicRSAKeyForJWTValidation` in `test\test.dart`. Be sure to update this value and to delete the old cloud apps if you are generating tokens with newly created key pair. + +2. Generate tokens with dart code using the private key: + * Create a new dart app. + * Use [dart_jsonwebtoken](https://pub.dev/packages/dart_jsonwebtoken) package. + * Run `dart pub add dart_jsonwebtoken` + * Code sample: + ```dart + String username = "username@realm.io"; + String userId = ObjectId().toString(); + + final jwt = JWT( + { + "sub": userId, + "name": {"firstName": "John", "lastName": "Doe"}, + "email": username, + "gender": "male", + "birthDay": "1999-10-11", + "minAge": "10", + "maxAge": "90", + "company": "Realm", + }, + issuer: 'https://realm.io', + audience: Audience(["mongodb.com"]), + )..header = {"kid": "1"}; + + // Paste here private key string from private_key.pem file + String privateKey = '''-----BEGIN RSA PRIVATE KEY----- + MIIEowIBAAKCAQEAvNHHs8T0AHD7SJ+CKvVRleeJa4wqYTnaVYV+5bX9FmFXVoN+ + ........ + -----END RSA PRIVATE KEY-----'''; + var token = jwt.sign(RSAPrivateKey(privateKey), algorithm: JWTAlgorithm.RS256, expiresIn: Duration(minutes: 3)); + print('Signed token: $token\n'); + ``` + * Once you have the token you can use it to login in the tests with `app.login(Credentials.jwt(token))`. Be sure that the public key configured in the cloud apps is from the correct key pair and corresponds to the private key used for generating this token. + diff --git a/test/credentials_test.dart b/test/credentials_test.dart index ccf8342c1..66fbdd443 100644 --- a/test/credentials_test.dart +++ b/test/credentials_test.dart @@ -309,4 +309,143 @@ Future main([List? args]) async { await authProvider.callResetPasswordFunction(username, newPassword); }, throws("failed to reset password for user $username")); }, appName: AppNames.autoConfirm); + + /// JWT Payload data + /// { + /// "sub": "62f394e9bcb9fee0c9aecb76", //User.identities.id: If it is a new id the user is created, if it is an existing id the user and profile are updated. + /// "name": { + /// "firstName": "John", + /// "lastName": "Doe" + /// }, + /// "email": "JWT_privatekey_validated_user@realm.io", + /// "gender": "male", + /// "birthDay": "1999-10-11", + /// "minAge": "10", + /// "maxAge": "90", + /// "company": "Realm", + /// "iat": 1660145686, + /// "exp": 4813745686, //100 years after Aug 2022 + /// "aud": "mongodb.com", + /// "iss": "https://realm.io" + /// } + /// JWT with private key validation is configured in flexible app wich is used by the tests by default. + baasTest('JWT validation by specified public key - login', (configuration) async { + final app = App(configuration); + String username = "JWT_privatekey_validated_user@realm.io"; + String userId = "62f394e9bcb9fee0c9aecb76"; + var token = + "eyJraWQiOiIxIiwiYWxnIjoiUlMyNTYiLCJ0eXAiOiJKV1QifQ.eyJzdWIiOiI2MmYzOTRlOWJjYjlmZWUwYzlhZWNiNzYiLCJuYW1lIjp7ImZpcnN0TmFtZSI6IkpvaG4iLCJsYXN0TmFtZSI6IkRvZSJ9LCJlbWFpbCI6IkpXVF9wcml2YXRla2V5X3ZhbGlkYXRlZF91c2VyQHJlYWxtLmlvIiwiZ2VuZGVyIjoibWFsZSIsImJpcnRoRGF5IjoiMTk5OS0xMC0xMSIsIm1pbkFnZSI6IjEwIiwibWF4QWdlIjoiOTAiLCJjb21wYW55IjoiUmVhbG0iLCJpYXQiOjE2NjAxNDU2ODYsImV4cCI6NDgxMzc0NTY4NiwiYXVkIjoibW9uZ29kYi5jb20iLCJpc3MiOiJodHRwczovL3JlYWxtLmlvIn0.NAy60d4zpzRyJayO9qa6i7T3Yui4vrEJNK5FYhlQGAPCCKmPpBBrPZnOH2QwTsE1sW5jr9EsUPix6PLIauSY4nE-s4JrFb9Yu1QmhzYiXAzzyRK_yJOLmrOujqnWb57Z1KvZo5CsUafTgB5-mbs4t4-udIZEubEgr7sgH51rHK7F1r7EArwT3Fbx-EjPDTN1cWn4945Hku6wk0WgdXwVg6TEaNtT0RrEegw9t63sW1UvOYsgXpHfCePGH8VRX7yYYqu1xBnS1S1ZHNgGNZp3t8pu4lod6jHho0dPetAq9oMSmUP9H2uiKkwqFmWC_bVEjTxX4bGSbLGKZQRkiOn38w"; + final credentials = Credentials.jwt(token); + final user = await app.logIn(credentials); + + expect(user.state, UserState.loggedIn); + expect(user.identities[0].id, userId); + expect(user.provider, AuthProviderType.jwt); + expect(user.profile.email, username); + expect(user.profile.name, username); + expect(user.profile.gender, "male"); + expect(user.profile.birthDay, "1999-10-11"); + expect(user.profile.minAge, "10"); + expect(user.profile.maxAge, "90"); + expect(user.profile.firstName, "John"); + expect(user.profile.lastName, "Doe"); + expect(user.profile["company"], "Realm"); + }); + + /// JWT Payload data + /// { + /// "sub": "62f3840b4ac43f38a50b9e2b", //User.identities.id of the existing user realm-test@realm.io + /// "name": { + /// "firstName": "John", + /// "lastName": "Doe" + /// }, + /// "email": "jwt_user@#r@D@realm.io", + /// "gender": "male", + /// "birthDay": "1999-10-11", + /// "minAge": "10", + /// "maxAge": "90", + /// "company": "Realm", + /// "iat": 1660145055, + /// "exp": 4813745055, //100 years after Aug 2022 + /// "aud": "mongodb.com", + /// "iss": "https://realm.io" + /// } + baasTest('JWT - login with existing user and edit profile', (configuration) async { + final app = App(configuration); + final username = "jwt_user@#r@D@realm.io"; + final authProvider = EmailPasswordAuthProvider(app); + // Always register jwt_user@#r@D@realm.io as a new user. + try { + await authProvider.registerUser(username, strongPassword); + } on RealmException catch (e) { + { + if (e.message.contains("name already in use")) { + // If the user exists, delete it and register a new one with the same name and empty profile + final user1 = await loginWithRetry(app, Credentials.emailPassword(username, strongPassword)); + await app.deleteUser(user1); + await authProvider.registerUser(username, strongPassword); + } + } + } + final user = await app.logIn(Credentials.emailPassword(username, strongPassword)); + UserIdentity emailIdentity = user.identities.singleWhere((identity) => identity.provider == AuthProviderType.emailPassword); + expect(emailIdentity.provider, isNotNull); + var userId = emailIdentity.id; + + expect(user.state, UserState.loggedIn); + expect(user.provider, AuthProviderType.emailPassword); + expect(user.profile.email, username); + expect(user.profile.name, isNull); + expect(user.profile.gender, isNull); + expect(user.profile.birthDay, isNull); + expect(user.profile.minAge, isNull); + expect(user.profile.maxAge, isNull); + expect(user.profile.firstName, isNull); + expect(user.profile.lastName, isNull); + expect(user.profile["company"], isNull); + + var token = + "eyJraWQiOiIxIiwiYWxnIjoiUlMyNTYiLCJ0eXAiOiJKV1QifQ.eyJzdWIiOiI2MmYzODQwYjRhYzQzZjM4YTUwYjllMmIiLCJuYW1lIjp7ImZpcnN0TmFtZSI6IkpvaG4iLCJsYXN0TmFtZSI6IkRvZSJ9LCJlbWFpbCI6Imp3dF91c2VyQCNyQERAcmVhbG0uaW8iLCJnZW5kZXIiOiJtYWxlIiwiYmlydGhEYXkiOiIxOTk5LTEwLTExIiwibWluQWdlIjoiMTAiLCJtYXhBZ2UiOiI5MCIsImNvbXBhbnkiOiJSZWFsbSIsImlhdCI6MTY2MDE0NTA1NSwiZXhwIjo0ODEzNzQ1MDU1LCJhdWQiOiJtb25nb2RiLmNvbSIsImlzcyI6Imh0dHBzOi8vcmVhbG0uaW8ifQ.AHi4eh3wT9VifM0Hy07vVa2Sck8qlv4st71GaR5UFaytgDW7a-zhLRpPXYt6RX8mjzx6aCenbVr7-Cg8kKxL8XT5x-kmswse8FVtRXi-G5TU2C3AMuMTavP9KCMSpU6_IUfpF_i8kQbrke-YzfS5jflspyEgxHrHTcG0aRIRqBHAmu78er7t3MMv2tbScmipZv-QOXczhTBt0o2wk8iZ-qqTK2X6xb1wbhUS9YtY4oqmuE7n-I_1xah_yd4yF-aS3n13vT-nrm6aIdjwR_EVxAoekN9TTqs0WzpCjy2CcL-LO3RcepUCPQTGwKg9ObTFjJ2URw4FJ_BEA8EfpT_fBg"; + await user.linkCredentials(Credentials.jwt(token)); + + UserIdentity jwtIdentity = user.identities.singleWhere((identity) => identity.provider == AuthProviderType.jwt); + expect(jwtIdentity.provider, isNotNull); + var jwtUserId = jwtIdentity.id; + + var jwtUser = await app.logIn(Credentials.jwt(token)); + + expect(jwtUser.state, UserState.loggedIn); + expect(jwtUser.identities.singleWhere((identity) => identity.provider == AuthProviderType.jwt).id, jwtUserId); + expect(jwtUser.identities.singleWhere((identity) => identity.provider == AuthProviderType.emailPassword).id, userId); + expect(jwtUser.provider, AuthProviderType.jwt); + expect(jwtUser.profile.email, username); + expect(jwtUser.profile.name, username); + expect(jwtUser.profile.gender, "male"); + expect(jwtUser.profile.birthDay, "1999-10-11"); + expect(jwtUser.profile.minAge, "10"); + expect(jwtUser.profile.maxAge, "90"); + expect(jwtUser.profile.firstName, "John"); + expect(jwtUser.profile.lastName, "Doe"); + expect(jwtUser.profile["company"], "Realm"); + }, appName: AppNames.autoConfirm); + + /// Token signed with private key different than the one configured in Atlas 'flexible' app JWT authentication provider + /// JWT Payload data + /// { + /// "sub": "62f396888af8720b373ff06a", + /// "email": "wong_signiture_key@realm.io", + /// "iat": 1660142215, + /// "exp": 4813742215, //100 years after Aug 2022 + /// "aud": "mongodb.com", + /// "iss": "https://realm.io" + /// } + baasTest('JWT with wrong signiture key - login fails', (configuration) async { + final app = App(configuration); + var token = + "eyJraWQiOiIxIiwiYWxnIjoiUlMyNTYiLCJ0eXAiOiJKV1QifQ.eyJzdWIiOiI2MmYzOTY4ODhhZjg3MjBiMzczZmYwNmEiLCJlbWFpbCI6Indvbmdfc2lnbml0dXJlX2tleUByZWFsbS5pbyIsImlhdCI6MTY2MDE0MjIxNSwiZXhwIjo0ODEzNzQyMjE1LCJhdWQiOiJtb25nb2RiLmNvbSIsImlzcyI6Imh0dHBzOi8vcmVhbG0uaW8ifQ.Af--ZUCL_KC7lAhrD_d1lq91O7qVwu7GqXifwxKojkLCkbjmAER9K2Xa7BPO8xNstFeX8m9uBo4BCD5B6XmngSmyCj5OZWdiG5LTR_uhA3MnpqcV3Vu40K4Yx8XrjPuCL39xVPnEfPKLGz5TjEcMLa8xMPqo51byX0q3mR2eSS4w1A7c5TiTNuQ23_SCO8aK95SyXwuUmU4mH0iR4sHPtf64WyoAXkx8w5twXExzky1_h473CwtAERdMsBhwz1YzFKP0kxU31pg5SRciF5Ly66sK1fSPTMQPuVdS_wKvAYll8_trWnWS83M3_PWs4UxzOdjSpoK0uqhN-_IC38YOGg"; + final credentials = Credentials.jwt(token); + expect(() async { + await app.logIn(credentials); + }, throws("crypto/rsa: verification error")); + }); } diff --git a/test/test.dart b/test/test.dart index a928a3470..66cb7348a 100644 --- a/test/test.dart +++ b/test/test.dart @@ -171,6 +171,15 @@ const String argDifferentiator = "BAAS_DIFFERENTIATOR"; String testUsername = "realm-test@realm.io"; String testPassword = "123456"; +const String publicRSAKeyForJWTValidation = '''-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvNHHs8T0AHD7SJ+CKvVR +leeJa4wqYTnaVYV+5bX9FmFXVoN+vHbMLEteMvSw4L3kSRZdcqxY7cTuhlpAvkXP +Yq6qSI+bW8T4jGW963uCc83UhVMx4MH/PzipAlfcPjVO2u4c+dmpgZQpgEmA467u +tauXUhmTsGpgNg2Gvc61B7Ny4LphshsyrfaJ9WjA/NM6LOmEBW3JPNcVG2qyU+gt +O8BM8KOSx9wGyoGs4+OusvRkJizhPaIwa3FInLs4r+xZW9Bp6RndsmVECtvXRv5d +87ztpg6o3DZJRmTp2lAnkNLmxXlFkOSNIwiT3qqyRZOh4DuxPOpfg9K+vtFmRdEJ +RwIDAQAB +-----END PUBLIC KEY-----'''; enum AppNames { flexible, @@ -272,7 +281,7 @@ Future tryDeleteRealm(String path) async { } } - // TODO: File deletions does not work after tests so don't fail for now + // TODO: File deletions does not work after tests so don't fail for now https://github.com/realm/realm-dart/issues/751 // throw Exception('Failed to delete realm at path $path. Did you forget to close it?'); } @@ -325,6 +334,8 @@ Future setupBaas() async { ? BaasClient.docker(baasUrl, differentiator) : BaasClient.atlas(baasUrl, cluster, apiKey!, privateApiKey!, projectId!, differentiator)); + client.publicRSAKey = publicRSAKeyForJWTValidation; + var apps = await client.getOrCreateApps(); baasApps.addAll(apps); } @@ -342,7 +353,7 @@ Future baasTest( if (skip == null) { skip = url == null ? "BAAS URL not present" : false; } else if (skip is bool) { - skip = skip || url == null ? "BAAS URL not present" : false; + if (url == null) skip = "BAAS URL not present"; } test(name, () async {