浏览代码
Creating a staging branch, which will serve to contain the changes for reintroducing the packages and project ID that are scrubbed from the releases.
/main/staging
Creating a staging branch, which will serve to contain the changes for reintroducing the packages and project ID that are scrubbed from the releases.
/main/staging
nathaniel.buck@unity3d.com
3 年前
当前提交
80161691
共有 968 个文件被更改,包括 3849 次插入 和 5 次删除
-
69Packages/packages-lock.json
-
6ProjectSettings/ProjectSettings.asset
-
2ProjectSettings/UnityConnectSettings.asset
-
242Packages/com.unity.services.authentication/.README - External.md
-
53Packages/com.unity.services.authentication/CHANGELOG.md
-
7Packages/com.unity.services.authentication/CHANGELOG.md.meta
-
5Packages/com.unity.services.authentication/Documentation~/com.unity.services.authentication.md
-
8Packages/com.unity.services.authentication/Editor.meta
-
5Packages/com.unity.services.authentication/Editor/AssemblyInfo.cs
-
3Packages/com.unity.services.authentication/Editor/AssemblyInfo.cs.meta
-
298Packages/com.unity.services.authentication/Editor/AuthenticationAdminClient.cs
-
3Packages/com.unity.services.authentication/Editor/AuthenticationAdminClient.cs.meta
-
148Packages/com.unity.services.authentication/Editor/AuthenticationAdminNetworkClient.cs
-
11Packages/com.unity.services.authentication/Editor/AuthenticationAdminNetworkClient.cs.meta
-
17Packages/com.unity.services.authentication/Editor/AuthenticationIdentifier.cs
-
11Packages/com.unity.services.authentication/Editor/AuthenticationIdentifier.cs.meta
-
47Packages/com.unity.services.authentication/Editor/AuthenticationService.cs
-
11Packages/com.unity.services.authentication/Editor/AuthenticationService.cs.meta
-
292Packages/com.unity.services.authentication/Editor/AuthenticationSettingsElement.cs
-
3Packages/com.unity.services.authentication/Editor/AuthenticationSettingsElement.cs.meta
-
47Packages/com.unity.services.authentication/Editor/AuthenticationSettingsHelper.cs
-
3Packages/com.unity.services.authentication/Editor/AuthenticationSettingsHelper.cs.meta
-
58Packages/com.unity.services.authentication/Editor/AuthenticationSettingsProvider.cs
-
11Packages/com.unity.services.authentication/Editor/AuthenticationSettingsProvider.cs.meta
-
20Packages/com.unity.services.authentication/Editor/AuthenticationTopMenu.cs
-
11Packages/com.unity.services.authentication/Editor/AuthenticationTopMenu.cs.meta
-
79Packages/com.unity.services.authentication/Editor/IAuthenticationAdminClient.cs
-
3Packages/com.unity.services.authentication/Editor/IAuthenticationAdminClient.cs.meta
-
399Packages/com.unity.services.authentication/Editor/IdProviderElement.cs
-
3Packages/com.unity.services.authentication/Editor/IdProviderElement.cs.meta
-
8Packages/com.unity.services.authentication/Editor/Models.meta
-
49Packages/com.unity.services.authentication/Editor/Models/CreateIdProviderRequest.cs
-
11Packages/com.unity.services.authentication/Editor/Models/CreateIdProviderRequest.cs.meta
-
20Packages/com.unity.services.authentication/Editor/Models/DeleteIdProviderRequest.cs
-
11Packages/com.unity.services.authentication/Editor/Models/DeleteIdProviderRequest.cs.meta
-
19Packages/com.unity.services.authentication/Editor/Models/GetIdDomainResponse.cs
-
11Packages/com.unity.services.authentication/Editor/Models/GetIdDomainResponse.cs.meta
-
56Packages/com.unity.services.authentication/Editor/Models/IdProviderResponse.cs
-
11Packages/com.unity.services.authentication/Editor/Models/IdProviderResponse.cs.meta
-
19Packages/com.unity.services.authentication/Editor/Models/ListIdProviderResponse.cs
-
11Packages/com.unity.services.authentication/Editor/Models/ListIdProviderResponse.cs.meta
-
29Packages/com.unity.services.authentication/Editor/Models/TokenExchangeErrorResponse.cs
-
11Packages/com.unity.services.authentication/Editor/Models/TokenExchangeErrorResponse.cs.meta
-
16Packages/com.unity.services.authentication/Editor/Models/TokenExchangeRequest.cs
-
11Packages/com.unity.services.authentication/Editor/Models/TokenExchangeRequest.cs.meta
-
16Packages/com.unity.services.authentication/Editor/Models/TokenExchangeResponse.cs
-
11Packages/com.unity.services.authentication/Editor/Models/TokenExchangeResponse.cs.meta
-
44Packages/com.unity.services.authentication/Editor/Models/UpdateIdProviderRequest.cs
-
11Packages/com.unity.services.authentication/Editor/Models/UpdateIdProviderRequest.cs.meta
-
3Packages/com.unity.services.authentication/Editor/USS.meta
-
41Packages/com.unity.services.authentication/Editor/USS/AuthenticationStyleSheet.uss
-
3Packages/com.unity.services.authentication/Editor/USS/AuthenticationStyleSheet.uss.meta
-
3Packages/com.unity.services.authentication/Editor/UXML.meta
-
15Packages/com.unity.services.authentication/Editor/UXML/AuthenticationProjectSettings.uxml
-
3Packages/com.unity.services.authentication/Editor/UXML/AuthenticationProjectSettings.uxml.meta
-
12Packages/com.unity.services.authentication/Editor/UXML/IdProviderElement.uxml
-
3Packages/com.unity.services.authentication/Editor/UXML/IdProviderElement.uxml.meta
-
10Packages/com.unity.services.authentication/Editor/Unity.Services.Authentication.Editor.api
-
7Packages/com.unity.services.authentication/Editor/Unity.Services.Authentication.Editor.api.meta
-
23Packages/com.unity.services.authentication/Editor/Unity.Services.Authentication.Editor.asmdef
-
7Packages/com.unity.services.authentication/Editor/Unity.Services.Authentication.Editor.asmdef.meta
-
5Packages/com.unity.services.authentication/LICENSE.md
-
7Packages/com.unity.services.authentication/LICENSE.md.meta
-
79Packages/com.unity.services.authentication/README.md
-
7Packages/com.unity.services.authentication/README.md.meta
-
8Packages/com.unity.services.authentication/Runtime.meta
-
127Packages/com.unity.services.authentication/Runtime/AuthenticationAsyncOperation.cs
-
3Packages/com.unity.services.authentication/Runtime/AuthenticationAsyncOperation.cs.meta
-
58Packages/com.unity.services.authentication/Runtime/AuthenticationError.cs
-
3Packages/com.unity.services.authentication/Runtime/AuthenticationError.cs.meta
-
49Packages/com.unity.services.authentication/Runtime/AuthenticationException.cs
-
3Packages/com.unity.services.authentication/Runtime/AuthenticationException.cs.meta
-
209Packages/com.unity.services.authentication/Runtime/AuthenticationNetworkClient.cs
-
11Packages/com.unity.services.authentication/Runtime/AuthenticationNetworkClient.cs.meta
-
12Packages/com.unity.services.authentication/Runtime/AuthenticationService.cs
-
11Packages/com.unity.services.authentication/Runtime/AuthenticationService.cs.meta
-
645Packages/com.unity.services.authentication/Runtime/AuthenticationServiceInternal.cs
-
11Packages/com.unity.services.authentication/Runtime/AuthenticationServiceInternal.cs.meta
-
48Packages/com.unity.services.authentication/Runtime/CodeChallengeGenerator.cs
-
11Packages/com.unity.services.authentication/Runtime/CodeChallengeGenerator.cs.meta
-
8Packages/com.unity.services.authentication/Runtime/Core.meta
-
16Packages/com.unity.services.authentication/Runtime/Core/AccessTokenComponent.cs
-
3Packages/com.unity.services.authentication/Runtime/Core/AccessTokenComponent.cs.meta
-
51Packages/com.unity.services.authentication/Runtime/Core/AuthenticationPackageInitializer.cs
-
11Packages/com.unity.services.authentication/Runtime/Core/AuthenticationPackageInitializer.cs.meta
-
40Packages/com.unity.services.authentication/Runtime/Core/PlayerIdComponent.cs
-
3Packages/com.unity.services.authentication/Runtime/Core/PlayerIdComponent.cs.meta
-
35Packages/com.unity.services.authentication/Runtime/IAuthenticationService.cs
|
|||
# UPM Package Starter Kit |
|||
|
|||
The purpose of this starter kit is to provide the data structure and development guidelines for new packages meant for the **Unity Package Manager (UPM)**. |
|||
|
|||
## Are you ready to become a package? |
|||
The Package Manager is a work-in-progress for Unity and, in that sense, there are a few criteria that must be met for your package to be considered on the package list at this time: |
|||
- **Your code accesses public Unity C# APIs only.** If you have a native code component, it will need to ship with an official editor release. Internal API access might eventually be possible for Unity made packages, but not at this time. |
|||
- **Your code doesn't require security, obfuscation, or conditional access control.** Anyone should be able to download your package and access the source code. |
|||
|
|||
|
|||
## Package structure |
|||
|
|||
```none |
|||
<root> |
|||
├── package.json |
|||
├── README.md |
|||
├── CHANGELOG.md |
|||
├── LICENSE.md |
|||
├── Third Party Notices.md |
|||
├── QAReport.md |
|||
├── Editor |
|||
│ ├── Unity.[YourPackageName].Editor.asmdef |
|||
│ └── EditorExample.cs |
|||
├── Runtime |
|||
│ ├── Unity.[YourPackageName].asmdef |
|||
│ └── RuntimeExample.cs |
|||
├── Tests |
|||
│ ├── .tests.json |
|||
│ ├── Editor |
|||
│ │ ├── Unity.[YourPackageName].Editor.Tests.asmdef |
|||
│ │ └── EditorExampleTest.cs |
|||
│ └── Runtime |
|||
│ ├── Unity.[YourPackageName].Tests.asmdef |
|||
│ └── RuntimeExampleTest.cs |
|||
├── Samples |
|||
│ └── Example |
|||
│ ├── .sample.json |
|||
│ └── SampleExample.cs |
|||
└── Documentation~ |
|||
├── your-package-name.md |
|||
└── Images |
|||
``` |
|||
|
|||
## Develop your package |
|||
Package development works best within the Unity Editor. Here's how to set that up: |
|||
|
|||
1. Start **Unity**, create a local empty project. |
|||
|
|||
1. In a console (or terminal) application, go to the newly created project folder, then copy the contents of this starter kit into the packages directory. |
|||
__Note:__ Your directory name must be the name of your package (Example: `"com.unity.terrain-builder"`) |
|||
|
|||
1. ##### Fill in your package information |
|||
|
|||
Update the following required fields in file **package.json**: |
|||
- `"name"`: Package name, it should follow this naming convention: `"com.[YourCompanyName].[sub-group].[your-package-name]"` |
|||
(Example: `"com.unity.2d.animation"`, where `sub-group` should match the sub-group you selected in Gitlab) |
|||
- `"displayName"`: Package user friendly display name. (Example: `"Terrain Builder SDK"`). <br>__Note:__ Use a display name that will help users understand what your package is intended for. |
|||
- `"version"`: Package version `"X.Y.Z"`, your project **must** adhere to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). |
|||
Follow this guideline: |
|||
- To introduce a new feature or bug fix, increment the minor version (X.**Y**.Z) |
|||
- To introduce a breaking API change, increment the major version (**X**.Y.Z) |
|||
- The patch version (X.Y.**Z**), is reserved for sustainable engineering use only. |
|||
- `"unity"`: Unity Version your package is compatible with. (Example: `"2018.1"`) |
|||
- `"description"`: This description appears in the Package Manager window when the user selects this package from the list. For best results, use this text to summarize what the package does and how it can benefit the user.<br>__Note:__ Special formatting characters are supported, including line breaks (`\n`) and unicode characters such as bullets (`\u25AA`).<br> |
|||
|
|||
Update the following recommended fields in file **package.json**: |
|||
- `"dependencies"`: List of packages this package depends on. All dependencies will also be downloaded and loaded in a project with your package. Here's an example: |
|||
``` |
|||
dependencies: { |
|||
"com.unity.ads": "1.0.0" |
|||
"com.unity.analytics": "2.0.0" |
|||
} |
|||
``` |
|||
- `"keywords"`: List of words that will be indexed by the package manager search engine to facilitate discovery. |
|||
|
|||
Update the following field in file **Tests/.tests.json**: |
|||
- `"createSeparatePackage"`: If this is set to true, the CI will create a separate package for these tests. If you leave it set to false, the tests will remain part of the published package. If you set it to true, the tests in your package will automatically be moved to a separate package, and metadata will be added at publish time to link the packages together. This allows you to have a large number of tests, or assets, etc. that you don't want to include in your main package, while making it easy to test your package with those tests & fixtures. |
|||
|
|||
1. You should now see your package in the Project Window, along with all other available packages for your project. |
|||
|
|||
1. ##### Rename and update assembly definition files. |
|||
|
|||
Assembly definition files are used to generate C# assemblies during compilation. Package code must include asmdef files to ensure package code isolation. You can read up on assembly definition files [here](https://docs.unity3d.com/Manual/ScriptCompilationAssemblyDefinitionFiles.html). |
|||
|
|||
If your package contains Editor code, rename and modify [Editor/Unity.YourPackageName.Editor.asmdef](Editor/Unity.YourPackageName.Editor.asmdef). Otherwise, delete the Editor directory. |
|||
* Name **must** match your package name, suffixed by `.Editor` (i.e `Unity.[YourPackageName].Editor`) |
|||
* Assembly **must** reference `Unity.[YourPackageName]` (if you have any Runtime) |
|||
* Platforms **must** include `"Editor"` |
|||
|
|||
If your package contains code that needs to be included in Unity runtime builds, rename and modify [Runtime/Unity.YourPackageName.asmdef](Runtime/Unity.YourPackageName.asmdef). Otherwise, delete the Runtime directory. |
|||
* Name **must** match your package name (i.e `Unity.[YourPackageName]`) |
|||
|
|||
If your package has Editor code, you **must** include Editor Tests in your package. In that case, rename and modify [Tests/Editor/Unity.YourPackageName.Editor.Tests.asmdef](Tests/Editor/Unity.YourPackageName.Editor.Tests.asmdef). |
|||
* Name **must** match your package name, suffixed by `.Editor.Tests` (i.e `Unity.[YourPackageName].Editor.Tests`) |
|||
* Assembly **must** reference `Unity.[YourPackageName].Editor` and `Unity.[YourPackageName]` (if you have any Runtime) |
|||
* Platforms **must** include `"Editor"` |
|||
* Optional Unity references **must** include `"TestAssemblies"` to allow your Editor Tests to show up in the Test Runner/run on Katana when your package is listed in project manifest `testables` |
|||
|
|||
If your package has Runtime code, you **must** include Playmode Tests in your package. In that case, rename and modify [Tests/Runtime/Unity.YourPackageName.Tests.asmdef](Tests/Runtime/Unity.YourPackageName.Tests.asmdef). |
|||
* Name **must** match your package name, suffixed by `.Tests` (i.e `Unity.[YourPackageName].Tests`) |
|||
* Assembly **must** reference `Unity.[YourPackageName]` |
|||
* Optional Unity references **must** include `"TestAssemblies"` to allow your Playmode Tests to show up in the Test Runner/run on Katana when your package is listed in project manifest `testables` |
|||
|
|||
> |
|||
> The reason for choosing such name schema is to ensure that the name of the assembly built based on *assembly definition file* (_a.k.a .asmdef_) will follow the .Net [Framework Design Guidelines](https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/index) |
|||
|
|||
1. ##### Document your package. |
|||
|
|||
Rename and update **your-package-name.md** documentation file. Use this documentation template file to create preliminary, high-level documentation. This document is meant to introduce users to the features and sample files included in your package. Your package documentation files will be used to generate online and local docs, available from the package manager UI. |
|||
|
|||
**Document your public APIs** |
|||
* All public APIs need to be documented with XmlDoc. If you don't need an API to be accessed by clients, mark it as internal instead. |
|||
* API documentation is generated from [XmlDoc tags](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/xmldoc/xml-documentation-comments) included with all public APIs found in the package. See [Editor/EditorExample.cs](Editor/EditorExample.cs) for an example. |
|||
|
|||
**Documentation flow** |
|||
* Documentation needs to be ready when a publish request is sent to Release Management, as they will ask the documentation team to review it. |
|||
* The package will remain in `preview` mode until the final documentation is completed. Users will have access to the developer-generated documentation only in preview packages. |
|||
* When the documentation is completed, the documentation team will update the package git repo with the updates and they will publish it on the web. |
|||
* The package's development team will then need to submit a new package version with updated docs. |
|||
* The starting page in the user manual that links to package documentation is [Here](https://docs.unity3d.com/Manual/PackagesList.html). |
|||
* The `Documentation~` folder is suffixed with `~` so that its content does not get loaded in the editor, which is the recommended behavior. If this is problematic, you can still name it `Documentation` and all tools will still work correctly. `.Documentation` is also supported. |
|||
|
|||
**Test your documentation locally** |
|||
As you are developing your documentation, you can see what your documentation will look like by using the DocTools extension (optional). |
|||
Once the DocTools package is installed, it will add a `Generate Documentation` button in the Package Manager UI's details of your installed packages. To install the extension, follow these steps: |
|||
|
|||
1. Make sure you have `Package Manager UI v1.9.6` or above. |
|||
1. Your project manifest will need to point to a staging registry for this, which you can do by adding this line to it: `"registry": "https://staging-packages.unity.com"` |
|||
1. Install `Package Manager DocTools v1.0.0-preview.6` or above from the `Package Manager UI` (in the `All Packages` section). |
|||
1. After installation, you will see a `Generate Documentation` button which will generate the documentation locally, and open a web browser to a locally served version of your documentation so you can preview it. |
|||
1. (optional) If your package documentation contains multiple `.md` files for the user manual, see [this page](https://docs.unity3d.com/Packages/com.unity.package-manager-doctools@1.0/manual/index.html#table-of-content) to add a table of content to your documentation. |
|||
|
|||
The DocTools extension is still in preview, if you come across arguable results, please discuss them on #docs-packman. |
|||
|
|||
1. ##### Add samples to your package (code & assets). |
|||
If your package contains a sample, rename the `Samples/Example` folder, and update the `.sample.json` file in it. |
|||
|
|||
In the case where your package contains multiple samples, you can make a copy of the `Samples/Example` folder for each sample, and update the `.sample.json` file accordingly. |
|||
|
|||
Similar to `.tests.json` file, there is a `"createSeparatePackage"` field in `.sample.json`.If set to true, the CI will create an separate package for the sample.. |
|||
|
|||
Delete the `Samples` folder altogether if your package does not need samples. |
|||
|
|||
As of Unity release 2019.1, the /Samples directory of a package will be recognized by the package manager. Samples will not be imported to Unity when the package is added to a project, but will instead be offered to users of the package as an optional import, which can be added to their "/Assets" directory through a UI option. |
|||
|
|||
1. ##### Validate your package. |
|||
|
|||
Before you publish your package, you need to make sure that it passes all the necessary validation checks by using the Package Validation Suite extension (optional). |
|||
Once the Validation Suite package is installed, it will add a `Validate` button in the Package Manager UI's details of your installed packages. To install the extension, follow these steps: |
|||
|
|||
1. Make sure you have `Package Manager UI v1.9.6` or above. |
|||
1. Your project manifest will need to point to a staging registry for this, which you can do by adding this line to it: `"registry": "https://staging-packages.unity.com"` |
|||
1. Install `Package Validation Suite v0.3.0-preview.13` or above from the `Package Manager UI` in the `All Packages` section. If you can't find it there, try turning on `Show preview packages` in the `Advanced` menu. |
|||
1. After installation, you will see a `Validate` button show up in the Package Manager UI, which, when pressed, will run a series of tests and expose a `See Results` button for additional explanation. |
|||
1. If it succeeds, you will see a green bar with a `Success` message. |
|||
1. If it fails, you will see a red bar with a `Failed` message. |
|||
|
|||
The validation suite is still in preview, if you come across arguable results, please discuss them on #release-management. |
|||
|
|||
1. ##### Add tests to your package. |
|||
|
|||
All packages must contain tests. Tests are essential for Unity to ensure that the package works as expected in different scenarios. |
|||
|
|||
**Editor tests** |
|||
* Write all your Editor Tests in `Tests/Editor` |
|||
|
|||
**Playmode Tests** |
|||
* Write all your Playmode Tests in `Tests/Runtime`. |
|||
|
|||
1. ##### Update **CHANGELOG.md**. |
|||
|
|||
Every new feature or bug fix should have a trace in this file. For more details on the chosen changelog format, see [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). |
|||
|
|||
## Create a Pre-Release Package |
|||
Pre-Release Packages are a great way of getting your features in front of Unity Developers in order to get early feedback on functionality and UI designs. Pre-Release packages need to go through the publishing to production flow, as would any other package, but with diminished requirements. Here are the supported Pre-Release tags (to be used in package.json,`version` field), along with the requirements for each one: |
|||
|
|||
**Preview** - ex: `"version" : "1.2.0-preview"` |
|||
* Expected Package structure respected |
|||
* Package loads in Unity Editor without errors |
|||
* License file present - With third party notices file if necessary |
|||
* Test coverage is good - Optional but preferred |
|||
* Public APIs documented, minimal feature docs exists- Optional but preferred |
|||
|
|||
## Make sure your package meets all legal requirements |
|||
|
|||
##### Update **Third Party Notices.md** & **License.md** |
|||
|
|||
1. If your package has third-party elements and its licenses are approved, then all the licenses must be added to the `Third Party Notices.md` file. Simply duplicate the `Component Name/License Type/Provide License Details` section if you have more then one licenes. |
|||
|
|||
a. Concerning `[Provide License Details]` in the `Third Party Notices.md`, a URL can work as long as it actually points to the reproduced license and the copyright information _(if applicable)_. |
|||
|
|||
1. If your package does not have third party elements, you can remove the `Third Party Notices.md` file from your package. |
|||
|
|||
## Preparing your package for Staging |
|||
|
|||
Before publishing your package to production, you must send your package on the Package Manager's **staging** repository. The staging repository is monitored by QA and release management, and is where package validation will take place before it is accepted in production. |
|||
## *** IMPORTANT: The staging repository is publicly accessible, do not publish any packages with sensitive material you aren't ready to share with the public *** |
|||
|
|||
|
|||
1. Publishing your changes to the package manager's **staging** repository happens from Gitlab. To do so, simply setup your project's Continuous integration, which will be triggered by "Tags" on your branches. |
|||
* Join the **#devs-packman** channel on Slack, and request a staging **USERNAME** and **API_KEY**. |
|||
* In Gitlab, under the **Settings-> CI/CD -> Secret Variables** section, setup the following 2 project variables: |
|||
* API_KEY = [your API KEY] |
|||
* USER_NAME = [your USER NAME@unity] |
|||
* You're almost done! To publish a version of your package, make sure all your changes are checked into Gitlab, then create a new tag to reflect the version you are publishing (ex. "v1.2.2"), **the tag will trigger a publish to Staging**. You can view progress you the publish request by switch over to the "CI / CD" part of your project. |
|||
|
|||
1. Do it yourself CI |
|||
|
|||
If you are using your own CI, it is still recommended that you use the `build.sh` wrapper script that comes with the starter kit, as it handle the installation of the actual CI build scripts for you. |
|||
|
|||
Instead of calling `npm pack` and `npm publish` in the package root folder in your CI, use |
|||
``` |
|||
./build.sh package-ci pack --git-head $CI_COMMIT_SHA --git-url $CI_REPOSITORY_URL |
|||
``` |
|||
and |
|||
``` |
|||
./build.sh package-ci publish --git-head $CI_COMMIT_SHA --git-url $CI_REPOSITORY_URL |
|||
``` |
|||
respectively. |
|||
|
|||
1. Test your package locally |
|||
|
|||
Now that your package is published on the package manager's **staging** repository, you can test your package in the editor by creating a new project, and editing the project's `manifest.json` file to point to your staging package, as such: |
|||
``` |
|||
dependencies: { |
|||
"com.[YourCompanyName].[sub-group].[your-package-name]": "0.1.0" |
|||
}, |
|||
"registry": "https://staging-packages.unity.com" |
|||
``` |
|||
|
|||
## Get your package published to Production |
|||
|
|||
Packages are promoted to the **production** repository from **staging**, described above. |
|||
|
|||
Once you feel comfortable that your package is ready for prime time, and passes validation (Validation Suite), reach out to Unity so your package can be passed along to Release Management, for evaluation. |
|||
|
|||
**Release management will validate your package content, and check that the editor/playmode tests are passed before promoting the package to production. You will receive a confirmation email once the package is in production.** |
|||
|
|||
**You're almost done!** |
|||
At this point, your package is available on the cloud, but not discoverable through the editor: |
|||
|
|||
1. Contact the Package Manager team to ask them to add your package to the list of discoverable package for the Unity Editor. All you need to provide is the package name (com.[YourCompanyName].[sub-group].[your-package-name]) |
|
|||
# Changelog |
|||
All notable changes to this package will be documented in this file. |
|||
|
|||
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) |
|||
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). |
|||
|
|||
## [Unreleased] |
|||
|
|||
## [0.4.0-preview] - 2021-06-16 |
|||
### Changed |
|||
- Remove `SetOAuthClient()` as the authentication flow is simplified. |
|||
- Updated the initialization code to initialize with `UnityServices.Initialize()` |
|||
|
|||
## [0.4.0-preview] - 2021-06-07 |
|||
### Added |
|||
- Added Project Settings UI to configure ID providers. |
|||
- Added `SignInWithSteam`, `LinkWithSteam` functions. |
|||
- Changed the public interface of the Authentication service from a static instance and static methods to a singleton instance hidden behind an interface. |
|||
|
|||
### Changed |
|||
- Change the public signature of `Authentication` to return a Task, as opposed to a IAsyncOperation |
|||
- Change the public API names of `Authentication` to `Async` |
|||
|
|||
## [0.3.1-preview] - 2021-04-23 |
|||
### Changed |
|||
- Change the `SignInFailed` event to take `AuthenticationException` instead of a string as parameter. It can provide more information for debugging purposes. |
|||
- Fixed the `com.unity.services.core` package dependency version. |
|||
|
|||
## [0.3.0-preview] - 2021-04-21 |
|||
### Added |
|||
- Added `SignInWithApple`, `LinkWithApple`, `SignInWithGoogle`, `LinkWithGoogle`, `SignInWithFacebook`, `LinkWithFacebook` functions. |
|||
- Added `SignInWithSessionToken` |
|||
- Added error codes used by the social scenarios to `AuthenticationError`. |
|||
|
|||
## [0.2.3-preview] - 2021-03-23 |
|||
### Changed |
|||
- Rename the package from `com.unity.services.identity` to `com.unity.services.authentication`. Renamed the internal types/methods, too. |
|||
|
|||
## [0.2.2-preview] - 2021-03-15 |
|||
### Added |
|||
- Core package integration |
|||
|
|||
## [0.2.1-preview] - 2021-03-05 |
|||
|
|||
- Fixed dependency on Utilities package |
|||
|
|||
## [0.2.0-preview] - 2021-03-05 |
|||
|
|||
- Removed requirement for OAuth client ID to be specified (automatically uses project default OAuth client) |
|||
|
|||
## [0.1.0-preview] - 2021-01-18 |
|||
|
|||
### This is the first release of *com.unity.services.identity*. |
|
|||
fileFormatVersion: 2 |
|||
guid: 06e5cc5b0f38f824b835fc371283773e |
|||
TextScriptImporter: |
|||
externalObjects: {} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
# About Authentication SDK |
|||
|
|||
This is the Authentication SDK, a way to manage player accounts through the Unity User Authentication Service (UAS). |
|||
|
|||
This package is currently in pre-release state and is not guaranteed to be fully functional. |
|
|||
fileFormatVersion: 2 |
|||
guid: 9ab04733f4c8a4d7181b07dfa234aa46 |
|||
folderAsset: yes |
|||
DefaultImporter: |
|||
externalObjects: {} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using System; |
|||
using System.Runtime.CompilerServices; |
|||
|
|||
[assembly: InternalsVisibleTo("Unity.Services.Authentication.EditorTests")] |
|||
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // For Moq
|
|
|||
fileFormatVersion: 2 |
|||
guid: 80f9842e95c141e59bcc9d49c93cc2e1 |
|||
timeCreated: 1620833560 |
|
|||
using System; |
|||
using System.Reflection; |
|||
using System.Runtime.CompilerServices; |
|||
using Newtonsoft.Json; |
|||
using Unity.Services.Authentication.Editor.Models; |
|||
using Unity.Services.Authentication.Models; |
|||
using Unity.Services.Authentication.Utilities; |
|||
using Unity.Services.Core; |
|||
using UnityEditor; |
|||
using UnityEngine; |
|||
using ILogger = Unity.Services.Authentication.Utilities.ILogger; |
|||
using Logger = Unity.Services.Authentication.Utilities.Logger; |
|||
|
|||
[assembly: InternalsVisibleTo("Unity.Services.Authentication.Editor.Tests")] |
|||
[assembly: InternalsVisibleTo("Unity.Services.Authentication.EditorTests")] |
|||
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // For Moq
|
|||
|
|||
namespace Unity.Services.Authentication.Editor |
|||
{ |
|||
static class AuthenticationAdminClientManager |
|||
{ |
|||
internal static IAuthenticationAdminClient Instance { get; set; } = AuthenticationAdminClient(); |
|||
|
|||
static IAuthenticationAdminClient AuthenticationAdminClient() |
|||
{ |
|||
var logger = new Logger("[Authentication]"); |
|||
IDateTimeWrapper dateTime = new DateTimeWrapper(); |
|||
INetworkingUtilities networkUtilities = new NetworkingUtilities(null, logger); |
|||
string orgId = GetOrganizationId(); |
|||
var networkClient = new AuthenticationAdminNetworkClient("https://services.unity.com", |
|||
orgId, |
|||
CloudProjectSettings.projectId, |
|||
networkUtilities, |
|||
logger); |
|||
|
|||
return new AuthenticationAdminClient(logger, networkClient); |
|||
} |
|||
|
|||
// GetOrganizationId will gets the organization id associated with this Unity project.
|
|||
static string GetOrganizationId() |
|||
{ |
|||
// This is a temporary workaround to get the Genesis organization foreign key for non-DevX enhanced Unity versions.
|
|||
// When the eventual changes are backported into previous versions of Unity, this will no longer be necessary.
|
|||
Assembly assembly = Assembly.GetAssembly(typeof(EditorWindow)); |
|||
var unityConnectInstance = assembly.CreateInstance("UnityEditor.Connect.UnityConnect", false, BindingFlags.NonPublic | BindingFlags.Instance, null, null, null, null); Type t = unityConnectInstance.GetType(); |
|||
var projectInfo = t.GetProperty("projectInfo").GetValue(unityConnectInstance, null); |
|||
|
|||
Type projectInfoType = projectInfo.GetType(); |
|||
return projectInfoType.GetProperty("organizationForeignKey").GetValue(projectInfo, null) as string; |
|||
} |
|||
} |
|||
|
|||
class AuthenticationAdminClient : IAuthenticationAdminClient |
|||
{ |
|||
string m_IdDomain; |
|||
IAuthenticationAdminNetworkClient m_AuthenticationAdminNetworkClient; |
|||
ILogger m_Logger; |
|||
|
|||
string m_orgForeignKey; |
|||
string m_servicesGatewayToken; |
|||
string m_genesisToken; |
|||
|
|||
internal enum ServiceCalled |
|||
{ |
|||
TokenExchange, |
|||
AuthenticationAdmin |
|||
} |
|||
|
|||
public AuthenticationAdminClient(ILogger logger, IAuthenticationAdminNetworkClient networkClient, string genesisToken = "") |
|||
{ |
|||
m_Logger = logger; |
|||
m_AuthenticationAdminNetworkClient = networkClient; |
|||
m_genesisToken = genesisToken; |
|||
} |
|||
|
|||
public IAsyncOperation<string> GetIDDomain() |
|||
{ |
|||
var asyncOp = new AsyncOperation<string>(); |
|||
Action<string> getIdDomainFunc = token => |
|||
{ |
|||
var getDefaultIdDomainRequest = m_AuthenticationAdminNetworkClient.GetDefaultIdDomain(token); |
|||
getDefaultIdDomainRequest.Completed += request => HandleGetIdDomainAPICall(asyncOp, request); |
|||
}; |
|||
|
|||
getGenesisToken(); |
|||
var tokenAsyncOp = ExchangeToken(m_genesisToken); |
|||
tokenAsyncOp.Completed += tokenAsyncOpResult => getIdDomainFunc(tokenAsyncOpResult?.Result); |
|||
return asyncOp; |
|||
} |
|||
|
|||
public IAsyncOperation<IdProviderResponse> CreateIdProvider(string iddomain, CreateIdProviderRequest body) |
|||
{ |
|||
var asyncOp = new AsyncOperation<IdProviderResponse>(); |
|||
Action<string> createIdProviderFunc = token => |
|||
{ |
|||
var request = m_AuthenticationAdminNetworkClient.CreateIdProvider(body, iddomain, token); |
|||
request.Completed += req => HandleIdProviderResponseApiCall(asyncOp, req); |
|||
}; |
|||
|
|||
getGenesisToken(); |
|||
var tokenAsyncOp = ExchangeToken(m_genesisToken); |
|||
tokenAsyncOp.Completed += tokenAsyncOpResult => createIdProviderFunc(tokenAsyncOpResult?.Result); |
|||
return asyncOp; |
|||
} |
|||
|
|||
public IAsyncOperation<ListIdProviderResponse> ListIdProviders(string iddomain) |
|||
{ |
|||
var asyncOp = new AsyncOperation<ListIdProviderResponse>(); |
|||
Action<string> listIdProviderFunc = token => |
|||
{ |
|||
var request = m_AuthenticationAdminNetworkClient.ListIdProvider(iddomain, token); |
|||
request.Completed += req => HandleListIdProviderResponseApiCall(asyncOp, req); |
|||
}; |
|||
|
|||
getGenesisToken(); |
|||
var tokenAsyncOp = ExchangeToken(m_genesisToken); |
|||
tokenAsyncOp.Completed += tokenAsyncOpResult => listIdProviderFunc(tokenAsyncOpResult?.Result); |
|||
return asyncOp; |
|||
} |
|||
|
|||
public IAsyncOperation<IdProviderResponse> UpdateIdProvider(string iddomain, string type, UpdateIdProviderRequest body) |
|||
{ |
|||
var asyncOp = new AsyncOperation<IdProviderResponse>(); |
|||
Action<string> enableIdProviderFunc = token => |
|||
{ |
|||
var request = m_AuthenticationAdminNetworkClient.UpdateIdProvider(body, iddomain, type, token); |
|||
request.Completed += req => HandleIdProviderResponseApiCall(asyncOp, req); |
|||
}; |
|||
|
|||
getGenesisToken(); |
|||
var tokenAsyncOp = ExchangeToken(m_genesisToken); |
|||
tokenAsyncOp.Completed += tokenAsyncOpResult => enableIdProviderFunc(tokenAsyncOpResult?.Result); |
|||
return asyncOp; |
|||
} |
|||
|
|||
public IAsyncOperation<IdProviderResponse> EnableIdProvider(string iddomain, string type) |
|||
{ |
|||
var asyncOp = new AsyncOperation<IdProviderResponse>(); |
|||
Action<string> enableIdProviderFunc = token => |
|||
{ |
|||
var request = m_AuthenticationAdminNetworkClient.EnableIdProvider(iddomain, type, token); |
|||
request.Completed += req => HandleIdProviderResponseApiCall(asyncOp, req); |
|||
}; |
|||
|
|||
getGenesisToken(); |
|||
var tokenAsyncOp = ExchangeToken(m_genesisToken); |
|||
tokenAsyncOp.Completed += tokenAsyncOpResult => enableIdProviderFunc(tokenAsyncOpResult?.Result); |
|||
return asyncOp; |
|||
} |
|||
|
|||
public IAsyncOperation<IdProviderResponse> DisableIdProvider(string iddomain, string type) |
|||
{ |
|||
var asyncOp = new AsyncOperation<IdProviderResponse>(); |
|||
Action<string> disableIdProviderFunc = token => |
|||
{ |
|||
var request = m_AuthenticationAdminNetworkClient.DisableIdProvider(iddomain, type, token); |
|||
request.Completed += req => HandleIdProviderResponseApiCall(asyncOp, req); |
|||
}; |
|||
|
|||
getGenesisToken(); |
|||
var tokenAsyncOp = ExchangeToken(m_genesisToken); |
|||
tokenAsyncOp.Completed += tokenAsyncOpResult => disableIdProviderFunc(tokenAsyncOpResult?.Result); |
|||
return asyncOp; |
|||
} |
|||
|
|||
public IAsyncOperation<IdProviderResponse> DeleteIdProvider(string iddomain, string type) |
|||
{ |
|||
var asyncOp = new AsyncOperation<IdProviderResponse>(); |
|||
Action<string> deleteIdProviderFunc = token => |
|||
{ |
|||
var request = m_AuthenticationAdminNetworkClient.DeleteIdProvider(iddomain, type, token); |
|||
request.Completed += req => HandleIdProviderResponseApiCall(asyncOp, req); |
|||
}; |
|||
|
|||
getGenesisToken(); |
|||
var tokenAsyncOp = ExchangeToken(m_genesisToken); |
|||
tokenAsyncOp.Completed += tokenAsyncOpResult => deleteIdProviderFunc(tokenAsyncOpResult?.Result); |
|||
return asyncOp; |
|||
} |
|||
|
|||
public IdProviderResponse CloneIdProvider(IdProviderResponse x) |
|||
{ |
|||
return x.Clone(); |
|||
} |
|||
|
|||
internal IAsyncOperation<string> ExchangeToken(string token) |
|||
{ |
|||
var asyncOp = new AsyncOperation<string>(); |
|||
var request = m_AuthenticationAdminNetworkClient.TokenExchange(token); |
|||
request.Completed += req => HandleTokenExchange(asyncOp, req); |
|||
return asyncOp; |
|||
} |
|||
|
|||
void HandleGetIdDomainAPICall(AsyncOperation<string> asyncOp, IWebRequest<GetIdDomainResponse> request) |
|||
{ |
|||
if (HandleError(asyncOp, request, ServiceCalled.AuthenticationAdmin)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
m_IdDomain = request?.ResponseBody?.Id; |
|||
asyncOp.Succeed(request?.ResponseBody?.Id); |
|||
} |
|||
|
|||
void HandleTokenExchange(AsyncOperation<string> asyncOp, IWebRequest<TokenExchangeResponse> request) |
|||
{ |
|||
if (HandleError(asyncOp, request, ServiceCalled.TokenExchange)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var token = request?.ResponseBody?.Token; |
|||
m_servicesGatewayToken = token; |
|||
asyncOp.Succeed(token); |
|||
} |
|||
|
|||
void HandleIdProviderResponseApiCall(AsyncOperation<IdProviderResponse> asyncOp, IWebRequest<IdProviderResponse> request) |
|||
{ |
|||
if (HandleError(asyncOp, request, ServiceCalled.AuthenticationAdmin)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
asyncOp.Succeed(request?.ResponseBody); |
|||
} |
|||
|
|||
void HandleListIdProviderResponseApiCall(AsyncOperation<ListIdProviderResponse> asyncOp, IWebRequest<ListIdProviderResponse> request) |
|||
{ |
|||
if (HandleError(asyncOp, request, ServiceCalled.AuthenticationAdmin)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
asyncOp.Succeed(request?.ResponseBody); |
|||
} |
|||
|
|||
void HandleEmptyResponseApiCall(AsyncOperation<IdProviderResponse> asyncOp, IWebRequest<IdProviderResponse> request) |
|||
{ |
|||
if (HandleError(asyncOp, request, ServiceCalled.AuthenticationAdmin)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
asyncOp.Succeed(request?.ResponseBody); |
|||
} |
|||
|
|||
internal bool HandleError<Q, T>(AsyncOperation<Q> asyncOp, IWebRequest<T> request, ServiceCalled sc) |
|||
{ |
|||
if (!request.RequestFailed) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
if (request.NetworkError) |
|||
{ |
|||
asyncOp.Fail(new AuthenticationException(AuthenticationError.NetworkError)); |
|||
return true; |
|||
} |
|||
m_Logger?.Error("Error message: " + request.ErrorMessage); |
|||
|
|||
try |
|||
{ |
|||
switch (sc) |
|||
{ |
|||
case ServiceCalled.TokenExchange: |
|||
var tokenExchangeErrorResponse = JsonConvert.DeserializeObject<TokenExchangeErrorResponse>(request.ErrorMessage); |
|||
asyncOp.Fail(new AuthenticationException(tokenExchangeErrorResponse.Name, tokenExchangeErrorResponse.Message)); |
|||
break; |
|||
case ServiceCalled.AuthenticationAdmin: |
|||
var authenticationAdminErrorResponse = JsonConvert.DeserializeObject<AuthenticationErrorResponse>(request.ErrorMessage); |
|||
asyncOp.Fail(new AuthenticationException(authenticationAdminErrorResponse.Title, authenticationAdminErrorResponse.Detail)); |
|||
break; |
|||
default: |
|||
asyncOp.Fail(new AuthenticationException(AuthenticationError.UnknownError, "Unknown error")); |
|||
break; |
|||
} |
|||
} |
|||
catch (JsonException ex) |
|||
{ |
|||
asyncOp.Fail(new AuthenticationException(AuthenticationError.UnknownError, "Failed to deserialize server response: " + request.ErrorMessage)); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
asyncOp.Fail(new AuthenticationException(AuthenticationError.UnknownError, "Unknown error deserializing server response: " + request.ErrorMessage)); |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
void getGenesisToken() |
|||
{ |
|||
if (m_genesisToken == "") |
|||
{ |
|||
m_genesisToken = CloudProjectSettings.accessToken; |
|||
} |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: ca4a5cace103447794de02de329b8eeb |
|||
timeCreated: 1620234241 |
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Unity.Services.Authentication.Editor.Models; |
|||
using Unity.Services.Authentication.Utilities; |
|||
using UnityEngine; |
|||
using ILogger = Unity.Services.Authentication.Utilities.ILogger; |
|||
|
|||
namespace Unity.Services.Authentication.Editor |
|||
{ |
|||
interface IAuthenticationAdminNetworkClient |
|||
{ |
|||
IWebRequest<TokenExchangeResponse> TokenExchange(string token); |
|||
IWebRequest<GetIdDomainResponse> GetDefaultIdDomain(string token); |
|||
IWebRequest<IdProviderResponse> CreateIdProvider(CreateIdProviderRequest body, string idDomain, string token); |
|||
IWebRequest<ListIdProviderResponse> ListIdProvider(string idDomain, string token); |
|||
IWebRequest<IdProviderResponse> UpdateIdProvider(UpdateIdProviderRequest body, string idDomain, string type, string token); |
|||
IWebRequest<IdProviderResponse> EnableIdProvider(string idDomain, string type, string token); |
|||
IWebRequest<IdProviderResponse> DisableIdProvider(string idDomain, string type, string token); |
|||
IWebRequest<IdProviderResponse> DeleteIdProvider(string idDomain, string type, string token); |
|||
} |
|||
|
|||
class AuthenticationAdminNetworkClient : IAuthenticationAdminNetworkClient |
|||
{ |
|||
const string k_ServicesGatewayStem = "/api/player-identity/v1/organizations/"; |
|||
const string k_GetDefaultIdDomainStem = "/iddomains/default"; |
|||
const string k_TokenExchangeStem = "/api/auth/v1/genesis-token-exchange/unity"; |
|||
|
|||
readonly string m_ServicesGatewayHost; |
|||
|
|||
readonly string m_GetDefaultIdDomainUrl; |
|||
readonly string m_TokenExchangeUrl; |
|||
|
|||
readonly string m_OrganizationId; |
|||
readonly string m_ProjectId; |
|||
|
|||
readonly INetworkingUtilities m_NetworkClient; |
|||
|
|||
readonly Dictionary<string, string> m_CommonPlayerIdentityHeaders; |
|||
|
|||
internal AuthenticationAdminNetworkClient(string servicesGatewayHost, |
|||
string organizationId, |
|||
string projectId, |
|||
INetworkingUtilities networkClient, |
|||
ILogger logger) |
|||
{ |
|||
m_ServicesGatewayHost = servicesGatewayHost; |
|||
m_OrganizationId = organizationId; |
|||
m_ProjectId = projectId; |
|||
|
|||
m_GetDefaultIdDomainUrl = servicesGatewayHost + k_ServicesGatewayStem + organizationId + k_GetDefaultIdDomainStem; |
|||
m_TokenExchangeUrl = servicesGatewayHost + k_TokenExchangeStem; |
|||
m_NetworkClient = networkClient; |
|||
|
|||
m_CommonPlayerIdentityHeaders = new Dictionary<string, string> |
|||
{ |
|||
["ProjectId"] = projectId, |
|||
// The Error-Version header enables RFC7807HttpError error responses
|
|||
["Error-Version"] = "v1" |
|||
}; |
|||
} |
|||
|
|||
public IWebRequest<GetIdDomainResponse> GetDefaultIdDomain(string token) |
|||
{ |
|||
return m_NetworkClient.Get<GetIdDomainResponse>(m_GetDefaultIdDomainUrl, addTokenHeader(m_CommonPlayerIdentityHeaders, token)); |
|||
} |
|||
|
|||
public IWebRequest<TokenExchangeResponse> TokenExchange(string token) |
|||
{ |
|||
var body = new TokenExchangeRequest(); |
|||
body.Token = token; |
|||
return m_NetworkClient.PostJson<TokenExchangeResponse>(m_TokenExchangeUrl, body); |
|||
} |
|||
|
|||
public IWebRequest<IdProviderResponse> CreateIdProvider(CreateIdProviderRequest body, string idDomain, string token) |
|||
{ |
|||
return m_NetworkClient.PostJson<IdProviderResponse>(CreateIdProviderUrl(idDomain), body, addTokenHeader(m_CommonPlayerIdentityHeaders, token)); |
|||
} |
|||
|
|||
public IWebRequest<ListIdProviderResponse> ListIdProvider(string idDomain, string token) |
|||
{ |
|||
return m_NetworkClient.Get<ListIdProviderResponse>(ListIdProviderUrl(idDomain), addTokenHeader(m_CommonPlayerIdentityHeaders, token)); |
|||
} |
|||
|
|||
public IWebRequest<IdProviderResponse> UpdateIdProvider(UpdateIdProviderRequest body, string idDomain, string type, string token) |
|||
{ |
|||
return m_NetworkClient.Put<IdProviderResponse>(UpdateIdProviderUrl(idDomain, type), body, addTokenHeader(m_CommonPlayerIdentityHeaders, token)); |
|||
} |
|||
|
|||
public IWebRequest<IdProviderResponse> EnableIdProvider(string idDomain, string type, string token) |
|||
{ |
|||
return m_NetworkClient.Post<IdProviderResponse>(EnableIdProviderUrl(idDomain, type), addJsonHeader(addTokenHeader(m_CommonPlayerIdentityHeaders, token))); |
|||
} |
|||
|
|||
public IWebRequest<IdProviderResponse> DisableIdProvider(string idDomain, string type, string token) |
|||
{ |
|||
return m_NetworkClient.Post<IdProviderResponse>(DisableIdProviderUrl(idDomain, type), addJsonHeader(addTokenHeader(m_CommonPlayerIdentityHeaders, token))); |
|||
} |
|||
|
|||
public IWebRequest<IdProviderResponse> DeleteIdProvider(string idDomain, string type, string token) |
|||
{ |
|||
return m_NetworkClient.Delete<IdProviderResponse>(DeleteIdProviderUrl(idDomain, type), addTokenHeader(m_CommonPlayerIdentityHeaders, token)); |
|||
} |
|||
|
|||
Dictionary<string, string> addTokenHeader(Dictionary<string, string> d, string token) |
|||
{ |
|||
var headers = new Dictionary<string, string>(d); |
|||
headers.Add("Authorization", "Bearer " + token); |
|||
return headers; |
|||
} |
|||
|
|||
Dictionary<string, string> addJsonHeader(Dictionary<string, string> d) |
|||
{ |
|||
var headers = new Dictionary<string, string>(d); |
|||
headers.Add("Content-Type", "application/json"); |
|||
return headers; |
|||
} |
|||
|
|||
string CreateIdProviderUrl(string idDomain) |
|||
{ |
|||
return m_ServicesGatewayHost + k_ServicesGatewayStem + m_OrganizationId + "/iddomains/" + idDomain + "/idps"; |
|||
} |
|||
|
|||
string ListIdProviderUrl(string idDomain) |
|||
{ |
|||
return m_ServicesGatewayHost + k_ServicesGatewayStem + m_OrganizationId + "/iddomains/" + idDomain + "/idps"; |
|||
} |
|||
|
|||
string UpdateIdProviderUrl(string idDomain, string type) |
|||
{ |
|||
return m_ServicesGatewayHost + k_ServicesGatewayStem + m_OrganizationId + "/iddomains/" + idDomain + "/idps/" + type; |
|||
} |
|||
|
|||
string DeleteIdProviderUrl(string idDomain, string type) |
|||
{ |
|||
return m_ServicesGatewayHost + k_ServicesGatewayStem + m_OrganizationId + "/iddomains/" + idDomain + "/idps/" + type; |
|||
} |
|||
|
|||
string EnableIdProviderUrl(string idDomain, string type) |
|||
{ |
|||
return m_ServicesGatewayHost + k_ServicesGatewayStem + m_OrganizationId + "/iddomains/" + idDomain + "/idps/" + type + "/enable"; |
|||
} |
|||
|
|||
string DisableIdProviderUrl(string idDomain, string type) |
|||
{ |
|||
return m_ServicesGatewayHost + k_ServicesGatewayStem + m_OrganizationId + "/iddomains/" + idDomain + "/idps/" + type + "/disable"; |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 45791e692f25c4a61adf3f50dc1397ac |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using System; |
|||
using Unity.Services.Core.Editor; |
|||
|
|||
namespace Unity.Services.Authentication.Editor |
|||
{ |
|||
/// <summary>
|
|||
/// Implementation of the <see cref="IEditorGameServiceIdentifier"/> for the Authentication package
|
|||
/// </summary>
|
|||
/// <remarks>This identifier MUST be public struct.</remarks>
|
|||
public struct AuthenticationIdentifier : IEditorGameServiceIdentifier |
|||
{ |
|||
/// <summary>
|
|||
/// Key for the Authentication package
|
|||
/// </summary>
|
|||
public string GetKey() => "Authentication"; |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 6ac6e0d63f116404caeed7d030ba58cb |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using System; |
|||
using Unity.Services.Core.Editor; |
|||
|
|||
namespace Unity.Services.Authentication.Editor |
|||
{ |
|||
class AuthenticationService : IEditorGameService |
|||
{ |
|||
/// <summary>
|
|||
/// Name of the service
|
|||
/// Used for error handling and service fetching
|
|||
/// </summary>
|
|||
public string Name => "Authentication Service"; |
|||
|
|||
/// <summary>
|
|||
/// Identifier for the service
|
|||
/// Used when registering and fetching the service
|
|||
/// </summary>
|
|||
public IEditorGameServiceIdentifier Identifier { get; } = new AuthenticationIdentifier(); |
|||
|
|||
/// <summary>
|
|||
/// Flag used to determine whether COPPA Compliance should be adhered to
|
|||
/// for this service
|
|||
/// </summary>
|
|||
public bool RequiresCoppaCompliance => false; |
|||
|
|||
/// <summary>
|
|||
/// Flag used to determine whether this service has a dashboard
|
|||
/// </summary>
|
|||
public bool HasDashboard => false; |
|||
|
|||
/// <summary>
|
|||
/// Getter for the formatted dashboard url
|
|||
/// If <see cref="HasDashboard"/> is false, this field only need return null or empty string
|
|||
/// </summary>
|
|||
/// <returns>The formatted URL</returns>
|
|||
public string GetFormattedDashboardUrl() |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// The enabler which allows the service to toggle on/off
|
|||
/// Can be set to null, in which case there would be no toggle
|
|||
/// </summary>
|
|||
public IEditorGameServiceEnabler Enabler { get; } = null; |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 8907e4a6f69d84349b9fdf60013bdd52 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Unity.Services.Authentication.Editor.Models; |
|||
using Unity.Services.Core; |
|||
using UnityEditor; |
|||
using UnityEditor.UIElements; |
|||
using UnityEngine; |
|||
using UnityEngine.UIElements; |
|||
|
|||
namespace Unity.Services.Authentication.Editor |
|||
{ |
|||
class AuthenticationSettingsElement : VisualElement |
|||
{ |
|||
const string k_Uxml = "Packages/com.unity.services.authentication/Editor/UXML/AuthenticationProjectSettings.uxml"; |
|||
const string k_Uss = "Packages/com.unity.services.authentication/Editor/USS/AuthenticationStyleSheet.uss"; |
|||
|
|||
IAuthenticationAdminClient m_AdminClient; |
|||
|
|||
string m_ProjectId; |
|||
string m_IdDomainId; |
|||
|
|||
// Whether skip the confirmation window for tests/automation.
|
|||
bool m_SkipConfirmation; |
|||
|
|||
TextElement m_WaitingTextElement; |
|||
TextElement m_ErrorTextElement; |
|||
VisualElement m_AddIdProviderContainer; |
|||
List<string> m_AddIdProviderTypeChoices; |
|||
PopupField<string> m_AddIdProviderType; |
|||
Button m_RefreshButton; |
|||
Button m_AddButton; |
|||
VisualElement m_IdProviderListContainer; |
|||
|
|||
/// <summary>
|
|||
/// The text to show when the settings is waitng for an async operation to finish.
|
|||
/// </summary>
|
|||
public TextElement WaitingTextElement => m_WaitingTextElement; |
|||
|
|||
/// <summary>
|
|||
/// The text to show when there is an error.
|
|||
/// </summary>
|
|||
public TextElement ErrorTextElement => m_ErrorTextElement; |
|||
|
|||
/// <summary>
|
|||
/// The add ID provider choices in the dropdown list.
|
|||
/// </summary>
|
|||
public IEnumerable<string> AddIdProviderTypeChoices => m_AddIdProviderTypeChoices; |
|||
|
|||
/// <summary>
|
|||
/// The add ID provider dropdown list.
|
|||
/// </summary>
|
|||
public PopupField<string> AddIdProviderType => m_AddIdProviderType; |
|||
|
|||
/// <summary>
|
|||
/// The button to refresh the ID provider list.
|
|||
/// </summary>
|
|||
public Button RefreshButton => m_RefreshButton; |
|||
|
|||
/// <summary>
|
|||
/// The button to add a new ID provider.
|
|||
/// </summary>
|
|||
public Button AddButton => m_AddButton; |
|||
|
|||
/// <summary>
|
|||
/// The container to add ID providers.
|
|||
/// </summary>
|
|||
public VisualElement IdProviderListContainer => m_IdProviderListContainer; |
|||
|
|||
/// <summary>
|
|||
/// Event triggered when the <cref="AuthenticationSettingsElement"/> starts or finishes waiting for an async operation.
|
|||
/// The first parameter of the callback is the sender.
|
|||
/// The second parameter is true if it starts waiting, and false if it finishes waiting.
|
|||
/// </summary>
|
|||
public event Action<AuthenticationSettingsElement, bool> Waiting; |
|||
|
|||
public AuthenticationSettingsElement(IAuthenticationAdminClient adminClient, string projectId, bool skipConfirmation = false) |
|||
{ |
|||
m_AdminClient = adminClient; |
|||
m_ProjectId = projectId; |
|||
m_SkipConfirmation = skipConfirmation; |
|||
|
|||
var containerAsset = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(k_Uxml); |
|||
if (containerAsset != null) |
|||
{ |
|||
var containerUI = containerAsset.CloneTree().contentContainer; |
|||
|
|||
var styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>(k_Uss); |
|||
if (styleSheet != null) |
|||
{ |
|||
containerUI.styleSheets.Add(styleSheet); |
|||
} |
|||
else |
|||
{ |
|||
throw new Exception("Asset not found: " + k_Uss); |
|||
} |
|||
|
|||
m_WaitingTextElement = containerUI.Q<TextElement>(className: "auth-progress"); |
|||
m_ErrorTextElement = containerUI.Q<TextElement>(className: "auth-error"); |
|||
|
|||
m_RefreshButton = containerUI.Q<Button>("id-provider-refresh"); |
|||
m_RefreshButton.clicked += RefreshIdProviders; |
|||
|
|||
m_AddButton = containerUI.Q<Button>("id-provider-add"); |
|||
m_AddButton.SetEnabled(false); |
|||
m_AddButton.clicked += AddIdProvider; |
|||
|
|||
m_IdProviderListContainer = containerUI.Q<VisualElement>(className: "auth-id-provider-list"); |
|||
|
|||
m_AddIdProviderContainer = containerUI.Q<VisualElement>("id-provider-type"); |
|||
|
|||
Add(containerUI); |
|||
} |
|||
else |
|||
{ |
|||
throw new Exception("Asset not found: " + k_Uxml); |
|||
} |
|||
} |
|||
|
|||
public void RefreshIdProviders() |
|||
{ |
|||
ShowWaiting(); |
|||
if (m_IdDomainId == null) |
|||
{ |
|||
GetIdDomain(); |
|||
} |
|||
else |
|||
{ |
|||
ListIdProviders(); |
|||
} |
|||
} |
|||
|
|||
void GetIdDomain() |
|||
{ |
|||
var asyncOp = m_AdminClient.GetIDDomain(); |
|||
asyncOp.Completed += OnGetIdDomainCompleted; |
|||
} |
|||
|
|||
void OnGetIdDomainCompleted(IAsyncOperation<string> asyncOp) |
|||
{ |
|||
if (asyncOp.Exception != null) |
|||
{ |
|||
OnError(asyncOp.Exception); |
|||
return; |
|||
} |
|||
|
|||
m_IdDomainId = asyncOp.Result; |
|||
ListIdProviders(); |
|||
} |
|||
|
|||
void ListIdProviders() |
|||
{ |
|||
var asyncOp = m_AdminClient.ListIdProviders(m_IdDomainId); |
|||
asyncOp.Completed += OnListIdProvidersCompleted; |
|||
} |
|||
|
|||
void OnListIdProvidersCompleted(IAsyncOperation<ListIdProviderResponse> asyncOp) |
|||
{ |
|||
if (asyncOp.Exception != null) |
|||
{ |
|||
OnError(asyncOp.Exception); |
|||
return; |
|||
} |
|||
m_IdProviderListContainer.Clear(); |
|||
|
|||
if (asyncOp.Result?.Results != null) |
|||
{ |
|||
foreach (var provider in asyncOp.Result.Results) |
|||
{ |
|||
CreateIdProviderElement(provider); |
|||
} |
|||
} |
|||
|
|||
UpdateAddIdproviderList(); |
|||
HideWaiting(); |
|||
} |
|||
|
|||
void UpdateAddIdproviderList() |
|||
{ |
|||
var unusedIdProviders = new List<string>(IdProviderType.All); |
|||
|
|||
foreach (var child in m_IdProviderListContainer.Children()) |
|||
{ |
|||
if (!(child is IdProviderElement)) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
var idProviderElement = (IdProviderElement)child; |
|||
unusedIdProviders.Remove(idProviderElement.SavedValue.Type); |
|||
} |
|||
unusedIdProviders.Sort(); |
|||
|
|||
m_AddIdProviderContainer.Clear(); |
|||
m_AddIdProviderTypeChoices = unusedIdProviders; |
|||
if (unusedIdProviders.Count == 0) |
|||
{ |
|||
m_AddButton.SetEnabled(false); |
|||
} |
|||
else |
|||
{ |
|||
if (unusedIdProviders.Count > 0) |
|||
{ |
|||
m_AddIdProviderType = new PopupField<string>(null, unusedIdProviders, 0); |
|||
m_AddIdProviderContainer.Add(m_AddIdProviderType); |
|||
} |
|||
m_AddButton.SetEnabled(true); |
|||
} |
|||
} |
|||
|
|||
void AddIdProvider() |
|||
{ |
|||
var idProvider = new IdProviderResponse |
|||
{ |
|||
New = true, |
|||
Type = m_AddIdProviderType.value |
|||
}; |
|||
|
|||
CreateIdProviderElement(idProvider); |
|||
} |
|||
|
|||
void OnError(Exception error) |
|||
{ |
|||
error = AuthenticationSettingsHelper.ExtractException(error); |
|||
|
|||
m_ErrorTextElement.style.display = DisplayStyle.Flex; |
|||
m_ErrorTextElement.text = AuthenticationSettingsHelper.ExceptionToString(error); |
|||
Debug.LogError(error); |
|||
HideWaiting(); |
|||
} |
|||
|
|||
void CreateIdProviderElement(IdProviderResponse idProvider) |
|||
{ |
|||
var options = IdProviderOptions.GetOptions(idProvider.Type); |
|||
if (options == null) |
|||
{ |
|||
// the SDK doesn't support the ID provider type yet. Skip.
|
|||
return; |
|||
} |
|||
|
|||
var idProviderElement = new IdProviderElement(m_IdDomainId, m_AdminClient, idProvider, options, m_SkipConfirmation); |
|||
m_IdProviderListContainer.Add(idProviderElement); |
|||
idProviderElement.Waiting += OnIdProviderWaiting; |
|||
idProviderElement.Deleted += OnIdProviderDeleted; |
|||
idProviderElement.Error += OnIdProviderError; |
|||
|
|||
m_IdProviderListContainer.Add(idProviderElement); |
|||
UpdateAddIdproviderList(); |
|||
} |
|||
|
|||
void OnIdProviderWaiting(IdProviderElement sender, bool waiting) |
|||
{ |
|||
if (waiting) |
|||
{ |
|||
ShowWaiting(); |
|||
} |
|||
else |
|||
{ |
|||
HideWaiting(); |
|||
} |
|||
} |
|||
|
|||
void OnIdProviderDeleted(IdProviderElement sender) |
|||
{ |
|||
m_IdProviderListContainer.Remove(sender); |
|||
UpdateAddIdproviderList(); |
|||
} |
|||
|
|||
void OnIdProviderError(IdProviderElement sender, Exception error) |
|||
{ |
|||
OnError(error); |
|||
} |
|||
|
|||
void ShowWaiting() |
|||
{ |
|||
// clear previous error when a new async action is triggered.
|
|||
m_ErrorTextElement.style.display = DisplayStyle.None; |
|||
m_ErrorTextElement.text = string.Empty; |
|||
|
|||
m_WaitingTextElement.style.display = DisplayStyle.Flex; |
|||
SetEnabled(false); |
|||
|
|||
Waiting?.Invoke(this, true); |
|||
} |
|||
|
|||
void HideWaiting() |
|||
{ |
|||
m_WaitingTextElement.style.display = DisplayStyle.None; |
|||
SetEnabled(true); |
|||
Waiting?.Invoke(this, false); |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 1f130c428a4b4feb9d46ecee3206cc34 |
|||
timeCreated: 1620752364 |
|
|||
using System; |
|||
|
|||
namespace Unity.Services.Authentication.Editor |
|||
{ |
|||
static class AuthenticationSettingsHelper |
|||
{ |
|||
internal static Exception ExtractException(Exception exception) |
|||
{ |
|||
var aggregatedException = exception as AggregateException; |
|||
if (aggregatedException == null) |
|||
{ |
|||
return exception; |
|||
} |
|||
|
|||
if (aggregatedException.InnerExceptions.Count > 1) |
|||
{ |
|||
// There are multiple exceptions aggregated, don't try to extract exception.
|
|||
return exception; |
|||
} |
|||
|
|||
// It returns the first exception.
|
|||
return aggregatedException.InnerException; |
|||
} |
|||
|
|||
internal static string ExceptionToString(Exception exception) |
|||
{ |
|||
var errorMessage = "[ERROR] "; |
|||
var currentError = exception; |
|||
var firstError = true; |
|||
while (currentError != null) |
|||
{ |
|||
if (!firstError) |
|||
{ |
|||
errorMessage += "\n---> "; |
|||
} |
|||
else |
|||
{ |
|||
firstError = false; |
|||
} |
|||
errorMessage += currentError.Message; |
|||
currentError = currentError.InnerException; |
|||
} |
|||
|
|||
return errorMessage; |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 7caccdcd0f744a8d911a745661de6c5c |
|||
timeCreated: 1621631992 |
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Unity.Services.Core.Editor; |
|||
using UnityEditor; |
|||
using UnityEngine; |
|||
using UnityEngine.UIElements; |
|||
|
|||
namespace Unity.Services.Authentication.Editor |
|||
{ |
|||
class AuthenticationSettingsProvider : EditorGameServiceSettingsProvider |
|||
{ |
|||
const string k_Title = "Authentication"; |
|||
|
|||
AuthenticationSettingsProvider(SettingsScope scopes, IEnumerable<string> keywords = null) |
|||
: base(GenerateProjectSettingsPath(k_Title), scopes, keywords) {} |
|||
|
|||
/// <summary>
|
|||
/// Accessor for the operate service
|
|||
/// Used to toggle and get dashboard access
|
|||
/// </summary>
|
|||
protected override IEditorGameService EditorGameService => EditorGameServiceRegistry.Instance.GetEditorGameService<AuthenticationIdentifier>(); |
|||
|
|||
/// <summary>
|
|||
/// Title shown in the header for the project settings
|
|||
/// </summary>
|
|||
protected override string Title => k_Title; |
|||
|
|||
/// <summary>
|
|||
/// Description show in the header for the project settings
|
|||
/// </summary>
|
|||
protected override string Description => "This package provides a system for working with the Unity User Authentication Service (UAS), including log-in, player ID and access token retrieval, and session persistence."; |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override VisualElement GenerateServiceDetailUI() |
|||
{ |
|||
var settingsElement = new AuthenticationSettingsElement(AuthenticationAdminClientManager.Instance, CloudProjectSettings.projectId); |
|||
settingsElement.RefreshIdProviders(); |
|||
|
|||
return settingsElement; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override VisualElement GenerateUnsupportedDetailUI() |
|||
{ |
|||
return GenerateServiceDetailUI(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Method which adds your settings provider to ProjectSettings
|
|||
/// </summary>
|
|||
/// <returns>A <see cref="AuthenticationSettingsProvider"/>.</returns>
|
|||
[SettingsProvider] |
|||
public static SettingsProvider CreateSettingsProvider() |
|||
{ |
|||
return new AuthenticationSettingsProvider(SettingsScope.Project); |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 78b5efd28d743406b8682bcb71c787a3 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
#if ENABLE_EDITOR_GAME_SERVICES
|
|||
using System; |
|||
using UnityEditor; |
|||
|
|||
namespace Unity.Services.Authentication.Editor |
|||
{ |
|||
static class AuthenticationTopMenu |
|||
{ |
|||
const int k_ConfigureMenuPriority = 100; |
|||
const int k_ToolsMenuPriority = k_ConfigureMenuPriority + 11; |
|||
const string k_ServiceMenuRoot = "Services/Authentication/"; |
|||
|
|||
[MenuItem(k_ServiceMenuRoot + "Configure", priority = k_ConfigureMenuPriority)] |
|||
static void ShowProjectSettings() |
|||
{ |
|||
SettingsService.OpenProjectSettings("Project/Services/Authentication"); |
|||
} |
|||
} |
|||
} |
|||
#endif
|
|
|||
fileFormatVersion: 2 |
|||
guid: f35b6d8fe19bd49b386370e507468195 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using System; |
|||
using Unity.Services.Authentication.Editor.Models; |
|||
using Unity.Services.Core; |
|||
|
|||
namespace Unity.Services.Authentication.Editor |
|||
{ |
|||
static class IdProviderType |
|||
{ |
|||
public const string Apple = "apple.com"; |
|||
public const string Facebook = "facebook.com"; |
|||
public const string Steam = "steampowered.com"; |
|||
public const string Google = "google.com"; |
|||
|
|||
public static readonly string[] All = |
|||
{ |
|||
Apple, |
|||
Facebook, |
|||
Google, |
|||
Steam |
|||
}; |
|||
} |
|||
|
|||
interface IAuthenticationAdminClient |
|||
{ |
|||
/// <summary>
|
|||
/// Get the ID domain associated with the project.
|
|||
/// </summary>
|
|||
/// <param name="projectId">The Unity project ID.</param>
|
|||
/// <returns>Async operation with the id domain ID as the result.</returns>
|
|||
IAsyncOperation<string> GetIDDomain(); |
|||
|
|||
/// <summary>
|
|||
/// Lists all ID providers created for the organization's specified ID domain
|
|||
/// </summary>
|
|||
/// <param name="iddomain">The ID domain ID</param>
|
|||
/// <returns>The list of ID Providers configured in the ID domain.</returns>
|
|||
IAsyncOperation<ListIdProviderResponse> ListIdProviders(string iddomain); |
|||
|
|||
/// <summary>
|
|||
/// Create a new ID provider for the organization's specified ID domain
|
|||
/// </summary>
|
|||
/// <param name="iddomain">The ID domain ID</param>
|
|||
/// <param name="request">The ID provider to create.</param>
|
|||
/// <returns>The ID Provider created.</returns>
|
|||
IAsyncOperation<IdProviderResponse> CreateIdProvider(string iddomain, CreateIdProviderRequest request); |
|||
|
|||
/// <summary>
|
|||
/// Update an ID provider for the organization's specified ID domain
|
|||
/// </summary>
|
|||
/// <param name="iddomain">The ID domain ID</param>
|
|||
/// <param name="request">The ID provider to create.</param>
|
|||
/// <returns>The ID Provider updated.</returns>
|
|||
IAsyncOperation<IdProviderResponse> UpdateIdProvider(string iddomain, string type, UpdateIdProviderRequest request); |
|||
|
|||
/// <summary>
|
|||
/// Enable an ID provider for the organization's specified ID domain
|
|||
/// </summary>
|
|||
/// <param name="iddomain">The ID domain ID</param>
|
|||
/// <param name="type">The type of the ID provider.</param>
|
|||
/// <returns>The ID Provider updated.</returns>
|
|||
IAsyncOperation<IdProviderResponse> EnableIdProvider(string iddomain, string type); |
|||
|
|||
/// <summary>
|
|||
/// Disable an ID provider for the organization's specified ID domain
|
|||
/// </summary>
|
|||
/// <param name="iddomain">The ID domain ID</param>
|
|||
/// <param name="type">The type of the ID provider.</param>
|
|||
/// <returns>The ID Provider updated.</returns>
|
|||
IAsyncOperation<IdProviderResponse> DisableIdProvider(string iddomain, string type); |
|||
|
|||
/// <summary>
|
|||
/// Delete a specific ID provider from the organization's specified ID domain
|
|||
/// </summary>
|
|||
/// <param name="iddomain">The ID domain ID</param>
|
|||
/// <param name="type">The type of the ID provider.</param>
|
|||
/// <returns>The async operation to check whether the task is done.</returns>
|
|||
IAsyncOperation<IdProviderResponse> DeleteIdProvider(string iddomain, string type); |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: a6e05710dedc4ca8904dd440ac8d523f |
|||
timeCreated: 1620232781 |
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Unity.Services.Authentication.Editor.Models; |
|||
using Unity.Services.Core; |
|||
using UnityEditor; |
|||
using UnityEngine; |
|||
using UnityEngine.UIElements; |
|||
|
|||
namespace Unity.Services.Authentication.Editor |
|||
{ |
|||
class IdProviderOptions |
|||
{ |
|||
public const string IdProviderApple = "apple.com"; |
|||
public const string IdProviderGoogle = "google.com"; |
|||
public const string IdProviderFacebook = "facebook.com"; |
|||
public const string IdProviderSteam = "steampowered.com"; |
|||
|
|||
public string IdProviderType { get; set; } |
|||
public string DisplayName { get; set; } |
|||
public string ClientIdDisplayName { get; set; } = "Client ID"; |
|||
|
|||
public string ClientSecretDisplayName { get; set; } = "Client Secret"; |
|||
public bool NeedClientSecret {get; set; } |
|||
|
|||
static readonly Dictionary<string, IdProviderOptions> s_IdProviderOptions = new Dictionary<string, IdProviderOptions> |
|||
{ |
|||
[IdProviderApple] = new IdProviderOptions |
|||
{ |
|||
IdProviderType = IdProviderApple, |
|||
DisplayName = "Sign-in with Apple", |
|||
ClientIdDisplayName = "App ID", |
|||
NeedClientSecret = false |
|||
}, |
|||
[IdProviderGoogle] = new IdProviderOptions |
|||
{ |
|||
IdProviderType = IdProviderGoogle, |
|||
DisplayName = "Google", |
|||
ClientIdDisplayName = "Client ID", |
|||
NeedClientSecret = false |
|||
}, |
|||
[IdProviderFacebook] = new IdProviderOptions |
|||
{ |
|||
IdProviderType = IdProviderFacebook, |
|||
DisplayName = "Facebook", |
|||
ClientIdDisplayName = "App ID", |
|||
ClientSecretDisplayName = "App Secret", |
|||
NeedClientSecret = true |
|||
}, |
|||
[IdProviderSteam] = new IdProviderOptions |
|||
{ |
|||
IdProviderType = IdProviderSteam, |
|||
DisplayName = "Steam", |
|||
ClientIdDisplayName = "App ID", |
|||
ClientSecretDisplayName = "Key", |
|||
NeedClientSecret = true |
|||
} |
|||
}; |
|||
|
|||
public static IdProviderOptions GetOptions(string idProviderType) |
|||
{ |
|||
if (!s_IdProviderOptions.ContainsKey(idProviderType)) |
|||
{ |
|||
return null; |
|||
} |
|||
return s_IdProviderOptions[idProviderType]; |
|||
} |
|||
} |
|||
|
|||
class IdProviderElement : VisualElement |
|||
{ |
|||
const string k_ElementUxml = "Packages/com.unity.services.authentication/Editor/UXML/IdProviderElement.uxml"; |
|||
|
|||
string m_IdDomainId; |
|||
IAuthenticationAdminClient m_AdminClient; |
|||
IdProviderOptions m_Options; |
|||
|
|||
Foldout m_Container; |
|||
Toggle m_Enabled; |
|||
TextField m_ClientId; |
|||
TextField m_ClientSecret; |
|||
Button m_SaveButton; |
|||
Button m_CancelButton; |
|||
Button m_DeleteButton; |
|||
|
|||
// Whether skip the confirmation window for tests/automation.
|
|||
bool m_SkipConfirmation; |
|||
|
|||
/// <summary>
|
|||
/// The foldout container to show or hide the ID provider details.
|
|||
/// </summary>
|
|||
public Foldout Container => m_Container; |
|||
|
|||
/// <summary>
|
|||
/// The toggle to control whether the ID provider is enabled.
|
|||
/// </summary>
|
|||
public Toggle EnabledToggle => m_Enabled; |
|||
|
|||
/// <summary>
|
|||
/// The text field to fill the client ID.
|
|||
/// </summary>
|
|||
public TextField ClientIdField => m_ClientId; |
|||
|
|||
/// <summary>
|
|||
/// The text field to fill the client secret.
|
|||
/// </summary>
|
|||
public TextField ClientSecretField => m_ClientSecret; |
|||
|
|||
/// <summary>
|
|||
/// The button to save the changes.
|
|||
/// </summary>
|
|||
public Button SaveButton => m_SaveButton; |
|||
|
|||
/// <summary>
|
|||
/// The button to cancel changes.
|
|||
/// </summary>
|
|||
public Button CancelButton => m_CancelButton; |
|||
|
|||
|
|||
/// <summary>
|
|||
/// The button to delete the current ID provider.
|
|||
/// </summary>
|
|||
public Button DeleteButton => m_DeleteButton; |
|||
|
|||
/// <summary>
|
|||
/// Event triggered when the <cref="IdProviderElement"/> starts or finishes waiting for an async operation.
|
|||
/// The first parameter of the callback is the sender.
|
|||
/// The second parameter is true if it starts waiting, and false if it finishes waiting.
|
|||
/// </summary>
|
|||
public event Action<IdProviderElement, bool> Waiting; |
|||
|
|||
/// <summary>
|
|||
/// Event triggered when the current <cref="IdProviderElement"/> needs to be deleted by the container.
|
|||
/// The parameter of the callback is the sender.
|
|||
/// </summary>
|
|||
public event Action<IdProviderElement> Deleted; |
|||
|
|||
/// <summary>
|
|||
/// Event triggered when the current <cref="IdProviderElement"/> catches an error.
|
|||
/// The first parameter of the callback is the sender.
|
|||
/// The second parameter is the exception caught by the element.
|
|||
/// </summary>
|
|||
public event Action<IdProviderElement, Exception> Error; |
|||
|
|||
/// <summary>
|
|||
/// The value saved on the server side.
|
|||
/// </summary>
|
|||
public IdProviderResponse SavedValue { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// The value of that is about to be saved to the server.
|
|||
/// </summary>
|
|||
public IdProviderResponse CurrentValue { get; set; } |
|||
|
|||
public bool Changed => |
|||
SavedValue?.Type != CurrentValue?.Type || |
|||
SavedValue?.Disabled != CurrentValue?.Disabled || |
|||
(SavedValue?.ClientId ?? "") != (CurrentValue?.ClientId ?? "") || |
|||
(SavedValue?.ClientSecret ?? "") != (CurrentValue?.ClientSecret ?? ""); |
|||
|
|||
public bool IsValid => |
|||
!string.IsNullOrEmpty(CurrentValue.ClientId) && |
|||
(!m_Options.NeedClientSecret || !string.IsNullOrEmpty(CurrentValue.ClientSecret)); |
|||
|
|||
public IdProviderElement(string idDomain, IAuthenticationAdminClient adminClient, IdProviderResponse savedValue, IdProviderOptions options, bool skipConfirmation = false) |
|||
{ |
|||
m_IdDomainId = idDomain; |
|||
m_AdminClient = adminClient; |
|||
m_Options = options; |
|||
m_SkipConfirmation = skipConfirmation; |
|||
|
|||
SavedValue = savedValue; |
|||
CurrentValue = SavedValue.Clone(); |
|||
|
|||
var containerAsset = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(k_ElementUxml); |
|||
if (containerAsset != null) |
|||
{ |
|||
var containerUI = containerAsset.CloneTree().contentContainer; |
|||
|
|||
m_Container = containerUI.Q<Foldout>(className: "auth-id-provider-details"); |
|||
m_Container.text = savedValue.Type; |
|||
|
|||
// If the ID Provider element is new, default to unfold.
|
|||
m_Container.value = SavedValue.New; |
|||
|
|||
m_Enabled = containerUI.Q<Toggle>("id-provider-enabled"); |
|||
m_Enabled.RegisterCallback<ChangeEvent<bool>>(OnEnabledChanged); |
|||
|
|||
m_ClientId = containerUI.Q<TextField>("id-provider-client-id"); |
|||
m_ClientId.label = options.ClientIdDisplayName; |
|||
m_ClientId.RegisterCallback<ChangeEvent<string>>(OnClientIdChanged); |
|||
|
|||
m_ClientSecret = containerUI.Q<TextField>("id-provider-client-secret"); |
|||
if (options.NeedClientSecret) |
|||
{ |
|||
m_ClientSecret.label = options.ClientSecretDisplayName; |
|||
m_ClientSecret.RegisterCallback<ChangeEvent<string>>(OnClientSecretChanged); |
|||
} |
|||
else |
|||
{ |
|||
m_ClientSecret.style.display = DisplayStyle.None; |
|||
} |
|||
|
|||
m_SaveButton = containerUI.Q<Button>("id-provider-save"); |
|||
m_SaveButton.SetEnabled(false); |
|||
m_SaveButton.clicked += OnSaveButtonClicked; |
|||
|
|||
m_CancelButton = containerUI.Q<Button>("id-provider-cancel"); |
|||
m_CancelButton.SetEnabled(false); |
|||
m_CancelButton.clicked += OnCancelButtonClicked; |
|||
|
|||
m_DeleteButton = containerUI.Q<Button>("id-provider-delete"); |
|||
m_DeleteButton.clicked += OnDeleteButtonClicked; |
|||
m_DeleteButton.SetEnabled(!savedValue.New); |
|||
|
|||
ResetCurrentValue(); |
|||
Add(containerUI); |
|||
} |
|||
else |
|||
{ |
|||
throw new Exception("Asset not found: " + k_ElementUxml); |
|||
} |
|||
} |
|||
|
|||
void RefreshButtons() |
|||
{ |
|||
bool hasChanges = Changed; |
|||
m_SaveButton.SetEnabled(hasChanges && IsValid); |
|||
m_CancelButton.SetEnabled(hasChanges || SavedValue.New); |
|||
if (SavedValue.New) |
|||
{ |
|||
m_DeleteButton.SetEnabled(false); |
|||
m_DeleteButton.style.display = DisplayStyle.None; |
|||
} |
|||
else |
|||
{ |
|||
m_DeleteButton.SetEnabled(true); |
|||
m_DeleteButton.style.display = DisplayStyle.Flex; |
|||
} |
|||
} |
|||
|
|||
void OnEnabledChanged(ChangeEvent<bool> e) |
|||
{ |
|||
CurrentValue.Disabled = !e.newValue; |
|||
RefreshButtons(); |
|||
} |
|||
|
|||
void OnClientIdChanged(ChangeEvent<string> e) |
|||
{ |
|||
CurrentValue.ClientId = e.newValue; |
|||
RefreshButtons(); |
|||
} |
|||
|
|||
void OnClientSecretChanged(ChangeEvent<string> e) |
|||
{ |
|||
CurrentValue.ClientSecret = e.newValue; |
|||
RefreshButtons(); |
|||
} |
|||
|
|||
void OnSaveButtonClicked() |
|||
{ |
|||
int option = DisplayDialogComplex("Save your changes", "Do you want to save the ID provider changes?", "Save", "Cancel", ""); |
|||
switch (option) |
|||
{ |
|||
case 0: |
|||
Waiting?.Invoke(this, true); |
|||
|
|||
if (SavedValue.New) |
|||
{ |
|||
var asyncOp = m_AdminClient.CreateIdProvider(m_IdDomainId, new CreateIdProviderRequest(CurrentValue)); |
|||
asyncOp.Completed += OnSaveCompleted; |
|||
} |
|||
else |
|||
{ |
|||
var body = new UpdateIdProviderRequest(CurrentValue); |
|||
var asyncOp = m_AdminClient.UpdateIdProvider(m_IdDomainId, CurrentValue.Type, body); |
|||
asyncOp.Completed += OnSaveCompleted; |
|||
} |
|||
break; |
|||
|
|||
case 1: |
|||
break; |
|||
|
|||
default: |
|||
Debug.LogError("Unrecognized option."); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
void OnSaveCompleted(IAsyncOperation<IdProviderResponse> asyncOp) |
|||
{ |
|||
if (asyncOp.Exception != null) |
|||
{ |
|||
Error?.Invoke(this, AuthenticationSettingsHelper.ExtractException(asyncOp.Exception)); |
|||
Waiting?.Invoke(this, false); |
|||
return; |
|||
} |
|||
|
|||
SavedValue = asyncOp.Result; |
|||
|
|||
// Check enable/disable status
|
|||
if (SavedValue.Disabled != CurrentValue.Disabled) |
|||
{ |
|||
SavedValue.ClientSecret = CurrentValue.ClientSecret; |
|||
asyncOp = CurrentValue.Disabled ? m_AdminClient.DisableIdProvider(m_IdDomainId, CurrentValue.Type) : m_AdminClient.EnableIdProvider(m_IdDomainId, CurrentValue.Type); |
|||
asyncOp.Completed += OnEnableDisableCompleted; |
|||
return; |
|||
} |
|||
|
|||
// Enable/disable is not changed
|
|||
ResetCurrentValue(); |
|||
RefreshButtons(); |
|||
Waiting?.Invoke(this, false); |
|||
} |
|||
|
|||
void OnEnableDisableCompleted(IAsyncOperation<IdProviderResponse> asyncOp) |
|||
{ |
|||
// Handle enable/disable exception
|
|||
if (asyncOp.Exception != null) |
|||
{ |
|||
Error?.Invoke(this, AuthenticationSettingsHelper.ExtractException(asyncOp.Exception)); |
|||
Waiting?.Invoke(this, false); |
|||
return; |
|||
} |
|||
|
|||
// Only reset current value when no exception
|
|||
SavedValue = asyncOp.Result; |
|||
ResetCurrentValue(); |
|||
RefreshButtons(); |
|||
Waiting?.Invoke(this, false); |
|||
} |
|||
|
|||
void OnCancelButtonClicked() |
|||
{ |
|||
if (SavedValue.New) |
|||
{ |
|||
// It's a new ID provider and it hasn't been saved to the server yet.
|
|||
// Simply trigger delete event to notify parent to remove the element from the list.
|
|||
Deleted?.Invoke(this); |
|||
return; |
|||
} |
|||
ResetCurrentValue(); |
|||
} |
|||
|
|||
void ResetCurrentValue() |
|||
{ |
|||
CurrentValue = SavedValue.Clone(); |
|||
m_Enabled.value = !CurrentValue.Disabled; |
|||
m_ClientId.value = CurrentValue.ClientId ?? ""; |
|||
m_ClientSecret.value = CurrentValue.ClientSecret ?? ""; |
|||
|
|||
RefreshButtons(); |
|||
} |
|||
|
|||
void OnDeleteButtonClicked() |
|||
{ |
|||
int option = DisplayDialogComplex("Delete Request", "Do you want to delete the ID Provider?", "Delete", "Cancel", ""); |
|||
switch (option) |
|||
{ |
|||
// Delete
|
|||
case 0: |
|||
Waiting?.Invoke(this, true); |
|||
var asyncOp = m_AdminClient.DeleteIdProvider(m_IdDomainId, CurrentValue.Type); |
|||
asyncOp.Completed += OnDeleteCompleted; |
|||
break; |
|||
|
|||
// Cancel
|
|||
case 1: |
|||
break; |
|||
|
|||
default: |
|||
Debug.LogError("Unrecognized option."); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
void OnDeleteCompleted(IAsyncOperation<IdProviderResponse> asyncOp) |
|||
{ |
|||
if (asyncOp.Exception != null) |
|||
{ |
|||
Error?.Invoke(this, AuthenticationSettingsHelper.ExtractException(asyncOp.Exception)); |
|||
Waiting?.Invoke(this, false); |
|||
return; |
|||
} |
|||
|
|||
// Simply trigger delete event to notify parent to remove the element from the list.
|
|||
Deleted?.Invoke(this); |
|||
ResetCurrentValue(); |
|||
Waiting?.Invoke(this, false); |
|||
} |
|||
|
|||
int DisplayDialogComplex(string title, string message, string ok, string cancel, string alt) |
|||
{ |
|||
if (Application.isBatchMode || m_SkipConfirmation) |
|||
return 0; |
|||
|
|||
return EditorUtility.DisplayDialogComplex(title, message, ok, cancel, alt); |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 47435640c6bd4c5085515305c09812b2 |
|||
timeCreated: 1620320241 |
|
|||
fileFormatVersion: 2 |
|||
guid: 4a4020826b7ea4ccaacf75ad3f61c1f3 |
|||
folderAsset: yes |
|||
DefaultImporter: |
|||
externalObjects: {} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using System; |
|||
using Newtonsoft.Json; |
|||
using UnityEngine.Scripting; |
|||
|
|||
namespace Unity.Services.Authentication.Editor.Models |
|||
{ |
|||
[Serializable] |
|||
class CreateIdProviderRequest |
|||
{ |
|||
[Preserve] |
|||
public CreateIdProviderRequest() {} |
|||
|
|||
[Preserve] |
|||
public CreateIdProviderRequest(IdProviderResponse body) |
|||
{ |
|||
ClientId = body.ClientId; |
|||
ClientSecret = body.ClientSecret; |
|||
Type = body.Type; |
|||
Disabled = body.Disabled; |
|||
} |
|||
|
|||
[JsonProperty("clientId")] |
|||
public string ClientId; |
|||
|
|||
[JsonProperty("clientSecret")] |
|||
public string ClientSecret; |
|||
|
|||
[JsonProperty("type")] |
|||
public string Type; |
|||
|
|||
[JsonProperty("disabled")] |
|||
public bool Disabled; |
|||
|
|||
public override bool Equals(Object obj) |
|||
{ |
|||
// Check for null and compare run-time types.
|
|||
if ((obj == null) || !GetType().Equals(obj.GetType())) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
CreateIdProviderRequest c = (CreateIdProviderRequest)obj; |
|||
return (ClientId == c.ClientId) && |
|||
(ClientSecret == c.ClientSecret) && |
|||
(Disabled == c.Disabled) && |
|||
(Type == c.Type); |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: be1b1eaa739554966a83f3094a060c10 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using System; |
|||
using Newtonsoft.Json; |
|||
using UnityEngine.Scripting; |
|||
|
|||
namespace Unity.Services.Authentication.Editor.Models |
|||
{ |
|||
[Serializable] |
|||
class DeleteIdProviderRequest |
|||
{ |
|||
[Preserve] |
|||
public DeleteIdProviderRequest() {} |
|||
|
|||
[JsonProperty("IdDomain")] |
|||
public string IdDomain; |
|||
|
|||
// string type
|
|||
[JsonProperty("type")] |
|||
public string Type; |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 8123abbc4107b4f96b214a0be8153b82 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using System; |
|||
using Newtonsoft.Json; |
|||
using UnityEngine.Scripting; |
|||
|
|||
namespace Unity.Services.Authentication.Editor.Models |
|||
{ |
|||
[Serializable] |
|||
class GetIdDomainResponse |
|||
{ |
|||
[Preserve] |
|||
public GetIdDomainResponse() {} |
|||
|
|||
[JsonProperty("id")] |
|||
public string Id; |
|||
|
|||
[JsonProperty("name")] |
|||
public string Name; |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 9014e1c2ef9b94c00bb9aacb76012679 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using System; |
|||
using Newtonsoft.Json; |
|||
using UnityEngine.Scripting; |
|||
|
|||
namespace Unity.Services.Authentication.Editor.Models |
|||
{ |
|||
[Serializable] |
|||
class IdProviderResponse |
|||
{ |
|||
[Preserve] |
|||
public IdProviderResponse() {} |
|||
|
|||
[JsonIgnore] |
|||
public bool New; |
|||
|
|||
[JsonProperty("clientId")] |
|||
public string ClientId; |
|||
|
|||
[JsonProperty("clientSecret")] |
|||
public string ClientSecret; |
|||
|
|||
[JsonProperty("type")] |
|||
public string Type; |
|||
|
|||
[JsonProperty("disabled")] |
|||
public bool Disabled; |
|||
|
|||
public IdProviderResponse Clone() |
|||
{ |
|||
return new IdProviderResponse |
|||
{ |
|||
New = New, |
|||
Type = Type, |
|||
ClientId = ClientId, |
|||
ClientSecret = ClientSecret, |
|||
Disabled = Disabled |
|||
}; |
|||
} |
|||
|
|||
public override bool Equals(Object obj) |
|||
{ |
|||
// Check for null and compare run-time types.
|
|||
if ((obj == null) || !GetType().Equals(obj.GetType())) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
IdProviderResponse c = (IdProviderResponse)obj; |
|||
return (New == c.New) && |
|||
(ClientId == c.ClientId) && |
|||
(ClientSecret == c.ClientSecret) && |
|||
(Disabled == c.Disabled) && |
|||
(Type == c.Type); |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 7eacdd408d38b48c899541aa578c5269 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using System; |
|||
using Newtonsoft.Json; |
|||
using UnityEngine.Scripting; |
|||
|
|||
namespace Unity.Services.Authentication.Editor.Models |
|||
{ |
|||
[Serializable] |
|||
class ListIdProviderResponse |
|||
{ |
|||
[Preserve] |
|||
public ListIdProviderResponse() {} |
|||
|
|||
[JsonProperty("total")] |
|||
public int Total; |
|||
|
|||
[JsonProperty("results")] |
|||
public IdProviderResponse[] Results; |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 2daa2da5cf49d49f39454c147fce718c |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using System; |
|||
using Newtonsoft.Json; |
|||
using UnityEngine.Scripting; |
|||
|
|||
namespace Unity.Services.Authentication.Editor.Models |
|||
{ |
|||
/// <summary>
|
|||
/// The model for error response from authentication server.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// There is another field "details" in the error response. It provides additional details
|
|||
/// to the error. It's ignored in this deserialized class since it's not needed by the client SDK.
|
|||
/// </remarks>
|
|||
[Serializable] |
|||
class TokenExchangeErrorResponse |
|||
{ |
|||
[Preserve] |
|||
public TokenExchangeErrorResponse() {} |
|||
|
|||
[JsonProperty("name")] |
|||
public string Name; |
|||
|
|||
[JsonProperty("message")] |
|||
public string Message; |
|||
|
|||
[JsonProperty("status")] |
|||
public int Status; |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: c6f0eee34b47a4cb8b94023a4898c02d |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using System; |
|||
using Newtonsoft.Json; |
|||
using UnityEngine.Scripting; |
|||
|
|||
namespace Unity.Services.Authentication.Editor.Models |
|||
{ |
|||
[Serializable] |
|||
class TokenExchangeRequest |
|||
{ |
|||
[Preserve] |
|||
public TokenExchangeRequest() {} |
|||
|
|||
[JsonProperty("token")] |
|||
public string Token; |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: e4bf54815194b46d4a6a172313afe524 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using System; |
|||
using Newtonsoft.Json; |
|||
using UnityEngine.Scripting; |
|||
|
|||
namespace Unity.Services.Authentication.Editor.Models |
|||
{ |
|||
[Serializable] |
|||
class TokenExchangeResponse |
|||
{ |
|||
[Preserve] |
|||
public TokenExchangeResponse() {} |
|||
|
|||
[JsonProperty("token")] |
|||
public string Token; |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: faea4b70d595b40938db3363e4ca22fa |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using System; |
|||
using Newtonsoft.Json; |
|||
using UnityEngine.Scripting; |
|||
|
|||
namespace Unity.Services.Authentication.Editor.Models |
|||
{ |
|||
[Serializable] |
|||
class UpdateIdProviderRequest |
|||
{ |
|||
[Preserve] |
|||
public UpdateIdProviderRequest() {} |
|||
|
|||
[Preserve] |
|||
public UpdateIdProviderRequest(IdProviderResponse body) |
|||
{ |
|||
ClientId = body.ClientId; |
|||
ClientSecret = body.ClientSecret; |
|||
Type = body.Type; |
|||
} |
|||
|
|||
[JsonProperty("clientId")] |
|||
public string ClientId; |
|||
|
|||
[JsonProperty("clientSecret")] |
|||
public string ClientSecret; |
|||
|
|||
[JsonProperty("type")] |
|||
public string Type; |
|||
|
|||
public override bool Equals(Object obj) |
|||
{ |
|||
// Check for null and compare run-time types.
|
|||
if ((obj == null) || !GetType().Equals(obj.GetType())) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
UpdateIdProviderRequest c = (UpdateIdProviderRequest)obj; |
|||
return (ClientId == c.ClientId) && |
|||
(ClientSecret == c.ClientSecret) && |
|||
(Type == c.Type); |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: bdcbfea2e2c424771b46cfa662b4a798 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
fileFormatVersion: 2 |
|||
guid: 82d2f74434ae476295725ecf6f372b2e |
|||
timeCreated: 1620232007 |
|
|||
.auth-progress { |
|||
-unity-text-align: middle-center; |
|||
font-size: 18px; |
|||
margin-top: 20px; |
|||
margin-bottom: 20px; |
|||
} |
|||
|
|||
.auth-error { |
|||
margin-top: 10px; |
|||
margin-bottom: 10px; |
|||
color: red; |
|||
display: none; |
|||
} |
|||
|
|||
.auth-id-providers-text { |
|||
font-size: 18px; |
|||
-unity-font-style: bold; |
|||
} |
|||
|
|||
.auth-id-provider-container { |
|||
flex-direction: column; |
|||
margin: 10px; |
|||
} |
|||
|
|||
.auth-id-provider-details { |
|||
margin-top: 10px; |
|||
} |
|||
|
|||
.auth-id-providers-title { |
|||
flex-direction: row; |
|||
} |
|||
|
|||
.auth-button-container { |
|||
flex-grow: 1; |
|||
flex-direction: row; |
|||
justify-content: flex-end; |
|||
} |
|||
|
|||
.auth-danger-button { |
|||
color: red; |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 72a4ecb8b8884295aa937c771cf80c39 |
|||
timeCreated: 1620232067 |
|
|||
fileFormatVersion: 2 |
|||
guid: de6fee8b1a0b4e6daa6017f3473d3ae4 |
|||
timeCreated: 1620232017 |
|
|||
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xmlns="UnityEngine.UIElements" editor-extension-mode="False"> |
|||
<ui:VisualElement class="auth-id-provider-container"> |
|||
<ui:TextElement text="Please wait..." class="auth-progress"/> |
|||
<ui:TextElement text="" class="auth-error"/> |
|||
<ui:VisualElement class="auth-id-providers-title"> |
|||
<ui:TextElement text="ID Providers" class="auth-id-providers-text"/> |
|||
<ui:VisualElement class="auth-button-container"> |
|||
<ui:VisualElement name="id-provider-type" /> |
|||
<ui:Button name="id-provider-add" text="Add" display-tooltip-when-elided="true" /> |
|||
<ui:Button name="id-provider-refresh" text="Refresh" display-tooltip-when-elided="true" /> |
|||
</ui:VisualElement> |
|||
</ui:VisualElement> |
|||
<ui:VisualElement class="auth-id-provider-list" /> |
|||
</ui:VisualElement> |
|||
</ui:UXML> |
|
|||
fileFormatVersion: 2 |
|||
guid: f0cb46d7f4ea4218b998906b544b49fa |
|||
timeCreated: 1620232088 |
|
|||
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns="UnityEngine.UIElements" editor-extension-mode="False"> |
|||
<ui:Foldout class="auth-id-provider-details" value="false"> |
|||
<ui:Toggle label="Enabled" name="id-provider-enabled" value="true"/> |
|||
<ui:TextField picking-mode="Ignore" label="Client ID" name="id-provider-client-id" max-length="128"/> |
|||
<ui:TextField picking-mode="Ignore" label="Client Secret" password="true" max-length="128" name="id-provider-client-secret"/> |
|||
<ui:VisualElement class="auth-button-container"> |
|||
<ui:Button name="id-provider-save" text="Save" display-tooltip-when-elided="true" /> |
|||
<ui:Button name="id-provider-cancel" text="Cancel" display-tooltip-when-elided="true" /> |
|||
<ui:Button name="id-provider-delete" text="Delete" display-tooltip-when-elided="true" class="auth-danger-button" /> |
|||
</ui:VisualElement> |
|||
</ui:Foldout> |
|||
</ui:UXML> |
|
|||
fileFormatVersion: 2 |
|||
guid: 061197ecd39a4e678b8a9f37cc2140bb |
|||
timeCreated: 1620322985 |
|
|||
// This file is generated. Do not modify by hand. |
|||
// XML documentation file not found. To check if public methods have XML comments, |
|||
// make sure the XML doc file is present and located next to the scraped dll |
|||
namespace Unity.Services.Authentication.Editor |
|||
{ |
|||
public struct AuthenticationIdentifier : Unity.Services.Core.Editor.IEditorGameServiceIdentifier |
|||
{ |
|||
public virtual string GetKey(); |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: afe1708907a3546638d2c4aa4d67b510 |
|||
DefaultImporter: |
|||
externalObjects: {} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
{ |
|||
"name": "Unity.Services.Authentication.Editor", |
|||
"rootNamespace": "", |
|||
"references": [ |
|||
"Unity.Services.Core.Editor", |
|||
"Unity.Services.Core", |
|||
"Unity.Services.Core.Internal", |
|||
"Unity.Settings.Editor", |
|||
"Unity.Services.Authentication", |
|||
"Unity.Services.Authentication.Models" |
|||
], |
|||
"includePlatforms": [ |
|||
"Editor" |
|||
], |
|||
"excludePlatforms": [], |
|||
"allowUnsafeCode": false, |
|||
"overrideReferences": false, |
|||
"precompiledReferences": [], |
|||
"autoReferenced": false, |
|||
"defineConstraints": [], |
|||
"versionDefines": [], |
|||
"noEngineReferences": false |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 2eed68446396f438a95293b27030ee23 |
|||
AssemblyDefinitionImporter: |
|||
externalObjects: {} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
com.unity.services.authentication copyright © 2021 Unity Technologies SF |
|||
|
|||
This software is subject to, and made available under, the Unity Operate Terms of Service (see https://unity3d.com/legal/one-operate-services-terms-of-service), and is an "Operate Service" as defined therein. |
|||
|
|||
Unless expressly provided otherwise, the software under this license is made available strictly on an "AS IS" BASIS WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED. Please review the terms of service for details on these and other terms and conditions. |
|
|||
fileFormatVersion: 2 |
|||
guid: e71fc55799574654a9fa4ea97bcff964 |
|||
TextScriptImporter: |
|||
externalObjects: {} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
# Unity Services Authentication SDK |
|||
|
|||
This package provides a system for working with the Unity User Authentication Service (UAS), including log-in, player ID and access token retrieval, and session persistence. |
|||
|
|||
## Integration |
|||
|
|||
The Authentication SDK is currently available on the UPM Candidates Repository. You will need to add the UPM Candidates Registry (https://artifactory.prd.it.unity3d.com/artifactory/api/npm/upm-candidates) as a Scoped Registry to your project. Once you have done that, you can add the package `com.unity.services.authentication` with the latest version: `0.5.0-preview`. |
|||
|
|||
Once you have installed the Authentication package, you must link your Unity project to a Unity Cloud Project using the Services window. |
|||
|
|||
The Authentication SDK automatically initializes itself on game start (no prefabs are required, and it will initialize regardless of the current scene), so the only integration steps are to start using the Authentication API in your code. The API is exposed via the `Authentication.Instance` object in the `Unity.Services.Authentication` namespace. |
|||
|
|||
Once the player has been signed in, the Authentication SDK will monitor the expiration time of their access token and attempt to refresh it automatically. No further action is required. |
|||
|
|||
On starting a game you may notice a `UnityServicesContainer` game object is created in the DontDestroyOnLoad area, with an Authentication component. This is how the Authentication SDK hooks onto the Unity lifecycle events that it requires, so if you destroy this object or any of its components, the Authentication system will cease to function. Some values are also cached into `PlayerPrefs`, so clearing all `PlayerPrefs` keys will require the player to sign in again from scratch on their next session rather than being able to continue their current session. |
|||
|
|||
## Public API |
|||
|
|||
### Sign-In API |
|||
|
|||
* `AuthenticationService.Instance.SignInAnonymouslyAsync()` |
|||
* This triggers the anonymous sign-in processes, which may take some seconds to finish |
|||
* This requires no parameters |
|||
* Anonymous sign-in stores the Session Token in Unity PlayerPrefs until an explicit SignOut call is made, so if you simply quit the game before, anonymous sign-in can use the Session Token to let the same anonymous user continue |
|||
* If you attempt to sign in while already signed in, this method will deliver a warning and set the async operation to fail with `AuthenticationError.ClientInvalidUserState`. |
|||
* `AuthenticationService.Instance.SignInWithSessionTokenAsync()` |
|||
* This triggers the sign-in of the user with the session token stored on the device. |
|||
* If there is no cached session token, the async operation fails with `AuthenticationError.ClientNoActiveSession`. |
|||
* If you attempt to sign in while already signed in, this method will deliver a warning and set the async operation to fail with `AuthenticationError.ClientInvalidUserState`. |
|||
* `AuthenticationService.Instance.SignInWithAppleAsync(string idToken)` |
|||
* This triggers the sign-in of the user with an ID token from Apple. |
|||
* Game developer is responsible for installing the necessary SDK and get the token from Apple. |
|||
* If you attempt to sign in while already signed in, this method will deliver a warning and set the async operation to fail with `AuthenticationError.ClientInvalidUserState`. |
|||
* `AuthenticationService.Instance.SignInWithGoogleAsync(string idToken)` |
|||
* This triggers the sign-in of the user with an ID token from Google. |
|||
* Game developer is responsible for installing the necessary SDK and get the token from Google. |
|||
* If you attempt to sign in while already signed in, this method will deliver a warning and set the async operation to fail with `AuthenticationError.ClientInvalidUserState`. |
|||
* `AuthenticationService.Instance.SignInWithFacebookAsync(string accessToken)` |
|||
* This triggers the sign-in of the user with an access token from Facebook. |
|||
* Game developer is responsible for installing the necessary SDK and get the token from Facebook. |
|||
* If you attempt to sign in while already signed in, this method will deliver a warning and set the async operation to fail with `AuthenticationError.ClientInvalidUserState`. |
|||
* `AuthenticationService.Instance.SignedIn` |
|||
* This is an event to which you can subscribe to be notified when the sign-in process has completed successfully |
|||
* `AuthenticationService.Instance.SignInFailed` |
|||
* This is an event to which you can subscribe to be notified when the sign-in process has failed for some reason |
|||
* `AuthenticationService.Instance.SignedOut` |
|||
* This is an event to which you can subscribe to be notified when the user has been signed out for some reason (either because `AuthenticationService.Instance.SignOut()` was called explicitly, or because of a rejection in an automatic system e.g. a refresh attempt was rejected for having an invalid token) |
|||
* `AuthenticationService.Instance.SignOut()` |
|||
* This triggers the sign-out process, which includes flushing all cached data and revocation of the access token |
|||
* If you are not signed in, this method will do nothing |
|||
* `AuthenticationService.Instance.LinkWithAppleAsync(string idToken)` |
|||
* This function links the current user with an ID token from Apple. The user can later sign-in with the linked Apple account. |
|||
* Game developer is responsible for installing the necessary SDK and get the token from Apple. |
|||
* If you attempt to link with an account that is already linked with another user, the async operation to fail with `AuthenticationError.EntityExists`. |
|||
* If you attempt to sign in while already signed in, this method will deliver a warning and set the async operation to fail with `AuthenticationError.ClientInvalidUserState`. |
|||
* `AuthenticationService.Instance.LinkWithGoogleAsync(string idToken)` |
|||
* This function links the current user with an ID token from Google. The user can later sign-in with the linked Google account. |
|||
* Game developer is responsible for installing the necessary SDK and get the token from Google. |
|||
* If you attempt to link with an account that is already linked with another user, the async operation to fail with `AuthenticationError.EntityExists`. |
|||
* If you attempt to sign in while already signed in, this method will deliver a warning and set the async operation to fail with `AuthenticationError.ClientInvalidUserState`. |
|||
* `AuthenticationService.Instance.LinkWithFacebookAsync(string accessToken)` |
|||
* This function links the current user with an access token from Facebook. The user can later sign-in with the linked Facebook account. |
|||
* Game developer is responsible for installing the necessary SDK and get the token from Facebook. |
|||
* If you attempt to link with an account that is already linked with another user, the async operation to fail with `AuthenticationError.EntityExists`. |
|||
* If you attempt to sign in while already signed in, this method will deliver a warning and set the async operation to fail with `AuthenticationError.ClientInvalidUserState`. |
|||
* `AuthenticationService.Instance.IsSignedIn` |
|||
* Returns true if the player is signed in |
|||
* Note that the player is still considered signed in until they explicitly call SignOut (or some automatic process causes explicit sign-out), so this will return true even if the access token has expired |
|||
* `AuthenticationService.Instance.PlayerId` |
|||
* This property exposes the ID of the player when they are signed in, or null if they are not |
|||
* `AuthenticationService.Instance.AccessToken` |
|||
* Returns the raw string of the current access token, or null if no valid token is available (e.g. the player is signed in but the token has expired and could not be refreshed) |
|||
* This value is updated automatically by the refresh process, so consumers should NOT cache this value |
|||
|
|||
### Additional Methods |
|||
|
|||
* `AuthenticationService.Instance.SetLogLevel(LogLevel level)` |
|||
* This enables verbose logging by the Authentication SDK, including exposing the underlying state machine transitions, web request successes and other details that might assist debugging |
|||
* By default this is set to Errors Only |
|
|||
fileFormatVersion: 2 |
|||
guid: 3f7aaf8e8c5a3ed4f82b405fb280a2b5 |
|||
TextScriptImporter: |
|||
externalObjects: {} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
fileFormatVersion: 2 |
|||
guid: 0c7e209fd9875be4f927a2a934c4f5ce |
|||
folderAsset: yes |
|||
DefaultImporter: |
|||
externalObjects: {} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using System; |
|||
using System.Collections; |
|||
using Unity.Services.Authentication.Utilities; |
|||
using Unity.Services.Core; |
|||
|
|||
namespace Unity.Services.Authentication |
|||
{ |
|||
class AuthenticationAsyncOperation : IAsyncOperation |
|||
{ |
|||
ILogger m_Logger; |
|||
AsyncOperation m_AsyncOperation; |
|||
AuthenticationException m_AuthenticationException; |
|||
|
|||
public AuthenticationAsyncOperation(ILogger logger) |
|||
{ |
|||
m_Logger = logger; |
|||
m_AsyncOperation = new AsyncOperation(); |
|||
m_AsyncOperation.SetInProgress(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Complete the operation as a failure.
|
|||
/// </summary>
|
|||
public void Fail(string errorCode, string message = null, Exception innerException = null) |
|||
{ |
|||
Fail(new AuthenticationException(errorCode, message, innerException)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Complete the operation as a failure with the exception.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Exception with type other than <see cref="AuthenticationException"/> are wrapped as
|
|||
/// an <see cref="AuthenticationException"/> with error code <code>AuthenticationError.UnknownError</code>.
|
|||
/// </remarks>
|
|||
public void Fail(Exception innerException) |
|||
{ |
|||
if (innerException is AuthenticationException) |
|||
{ |
|||
m_AuthenticationException = (AuthenticationException)innerException; |
|||
} |
|||
else |
|||
{ |
|||
m_AuthenticationException = new AuthenticationException(AuthenticationError.UnknownError, null, innerException); |
|||
} |
|||
LogAuthenticationException(m_AuthenticationException); |
|||
|
|||
BeforeFail?.Invoke(this); |
|||
m_AsyncOperation.Fail(m_AuthenticationException); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Complete this operation as a success.
|
|||
/// </summary>
|
|||
public void Succeed() |
|||
{ |
|||
m_AsyncOperation.Succeed(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// The event to invoke in case of failure right before marking the operation done.
|
|||
/// This is a good place to put some cleanup code before sending out the completed callback.
|
|||
/// </summary>
|
|||
public event Action<AuthenticationAsyncOperation> BeforeFail; |
|||
|
|||
/// <inheritdoc/>
|
|||
public bool IsDone |
|||
{ |
|||
get => m_AsyncOperation.IsDone; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public AsyncOperationStatus Status |
|||
{ |
|||
get => m_AsyncOperation.Status; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public event Action<IAsyncOperation> Completed |
|||
{ |
|||
add => m_AsyncOperation.Completed += value; |
|||
remove => m_AsyncOperation.Completed -= value; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// The exception that occured during the operation if it failed.
|
|||
/// The value can be set before the operation is done.
|
|||
/// </summary>
|
|||
public AuthenticationException Exception |
|||
{ |
|||
get => m_AuthenticationException; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
Exception IAsyncOperation.Exception |
|||
{ |
|||
get => m_AuthenticationException; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
bool IEnumerator.MoveNext() => !IsDone; |
|||
|
|||
/// <inheritdoc/>
|
|||
/// <remarks>
|
|||
/// Left empty because we don't support operation reset.
|
|||
/// </remarks>
|
|||
void IEnumerator.Reset() {} |
|||
|
|||
/// <inheritdoc/>
|
|||
object IEnumerator.Current => null; |
|||
|
|||
void LogAuthenticationException(AuthenticationException exception) |
|||
{ |
|||
if (m_Logger == null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var logMessage = exception.Message; |
|||
if (exception.InnerException != null) |
|||
{ |
|||
logMessage += $" cause: ${exception.InnerException}"; |
|||
} |
|||
m_Logger.Error(logMessage); |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: e7b99cac88a14e459ace5e1c4f22a015 |
|||
timeCreated: 1618596342 |
|
|||
using System; |
|||
|
|||
namespace Unity.Services.Authentication |
|||
{ |
|||
/// <summary>
|
|||
/// AuthenticationError lists the error codes to expect from <c>AuthenticationException</c> and failed events.
|
|||
/// </summary>
|
|||
public static class AuthenticationError |
|||
{ |
|||
/// <summary>
|
|||
/// This is a client error that is returned when the user is not in the right state.
|
|||
/// For example, calling SignOut when the user is already signed out will result in this error.
|
|||
/// </summary>
|
|||
public const string ClientInvalidUserState = "CLIENT_INVALID_USER_STATE"; |
|||
|
|||
/// <summary>
|
|||
/// This is a client error that is returned when trying to sign-in with session token while there is no cached
|
|||
/// session token.
|
|||
/// </summary>
|
|||
public const string ClientNoActiveSession = "CLIENT_NO_ACTIVE_SESSION"; |
|||
|
|||
/// <summary>
|
|||
/// The error returned when auth code parameter is not found in the authorize response.
|
|||
/// </summary>
|
|||
public const string AuthCodeNotFound = "AUTH_CODE_NOT_FOUND"; |
|||
|
|||
/// <summary>
|
|||
/// The error returned when the access token returned by server is invalid.
|
|||
/// </summary>
|
|||
public const string InvalidAccessToken = "INVALID_ACCESS_TOKEN"; |
|||
|
|||
/// <summary>
|
|||
/// The error returned when the entity with the same key already exists.
|
|||
/// It happens when a user tries to link a social account while the social account is already linked with another user.
|
|||
/// </summary>
|
|||
public const string EntityExists = "ENTITY_EXISTS"; |
|||
|
|||
/// <summary>
|
|||
/// The error returned when the parameter is missing or not in the right format.
|
|||
/// </summary>
|
|||
public const string InvalidParameters = "INVALID_PARAMETERS"; |
|||
|
|||
/// <summary>
|
|||
/// The error returned when the permission is denied using the token provided.
|
|||
/// </summary>
|
|||
public const string PermissionDenied = "PERMISSION_DENIED"; |
|||
|
|||
/// <summary>
|
|||
/// This is a network error when calling APIs.
|
|||
/// </summary>
|
|||
public const string NetworkError = "NETWORK_ERROR"; |
|||
|
|||
/// <summary>
|
|||
/// This is an unknown error. It happens when there is an unexpected server response.
|
|||
/// </summary>
|
|||
public const string UnknownError = "UNKNOWN_ERROR"; |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 67eb6c6266af413384b674771544e0bf |
|||
timeCreated: 1616186210 |
|
|||
using System; |
|||
|
|||
namespace Unity.Services.Authentication |
|||
{ |
|||
/// <summary>
|
|||
/// AuthenticationException represents a runtime exception from authentication.
|
|||
/// </summary>
|
|||
public class AuthenticationException : Exception |
|||
{ |
|||
/// <summary>
|
|||
/// The error code is the identifier for the type of error to handle.
|
|||
/// Checkout <c>AuthenticationError</c> for error codes.
|
|||
/// </summary>
|
|||
public string ErrorCode { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Constructor of the AuthenticationException with error code.
|
|||
/// </summary>
|
|||
/// <param name="errorCode">The error code for AuthenticationException.</param>
|
|||
public AuthenticationException(string errorCode) |
|||
: base(errorCode) |
|||
{ |
|||
ErrorCode = errorCode; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Constructor of the AuthenticationException with error code and a message.
|
|||
/// </summary>
|
|||
/// <param name="errorCode">The error code for AuthenticationException.</param>
|
|||
/// <param name="message">The additional message that helps to debug.</param>
|
|||
public AuthenticationException(string errorCode, string message) |
|||
: base(errorCode + ": " + message) |
|||
{ |
|||
ErrorCode = errorCode; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Constructor of the AuthenticationException with error code, a message and inner exception.
|
|||
/// </summary>
|
|||
/// <param name="errorCode">The error code for AuthenticationException.</param>
|
|||
/// <param name="message">The additional message that helps to debug.</param>
|
|||
/// <param name="innerException">The cause of the exception.</param>
|
|||
public AuthenticationException(string errorCode, string message, Exception innerException) |
|||
: base(errorCode + ": " + message, innerException) |
|||
{ |
|||
ErrorCode = errorCode; |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 6be08ac0648d4bc09c9100856ab323c1 |
|||
timeCreated: 1616186221 |
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Security.Cryptography; |
|||
using System.Text; |
|||
using Unity.Services.Authentication.Models; |
|||
using Unity.Services.Authentication.Utilities; |
|||
|
|||
namespace Unity.Services.Authentication |
|||
{ |
|||
interface IAuthenticationNetworkClient |
|||
{ |
|||
IWebRequest<WellKnownKeys> GetWellKnownKeys(); |
|||
IWebRequest<SignInResponse> SignInAnonymously(); |
|||
IWebRequest<SignInResponse> SignInWithSessionToken(string token); |
|||
IWebRequest<SignInResponse> SignInWithExternalToken(ExternalTokenRequest externalToken); |
|||
IWebRequest<SignInResponse> LinkWithExternalToken(string accessToken, ExternalTokenRequest externalToken); |
|||
} |
|||
|
|||
class AuthenticationNetworkClient : IAuthenticationNetworkClient |
|||
{ |
|||
const string k_WellKnownUrlStem = "/.well-known/jwks.json"; |
|||
const string k_AnonymousUrlStem = "/authentication/anonymous"; |
|||
const string k_SessionTokenUrlStem = "/authentication/session-token"; |
|||
const string k_ExternalTokenUrlStem = "/authentication/external-token"; |
|||
const string k_LinkExternalTokenUrlStem = "/authentication/link"; |
|||
const string k_OAuthUrlStem = "/oauth2/auth"; |
|||
const string k_OAuthTokenUrlStem = "/oauth2/token"; |
|||
const string k_OAuthScope = "openid offline unity.user identity.user"; |
|||
const string k_AuthResponseType = "code"; |
|||
const string k_ChallengeMethod = "S256"; |
|||
const string k_OauthRevokeStem = "/oauth2/revoke"; |
|||
|
|||
readonly INetworkingUtilities m_NetworkClient; |
|||
readonly ICodeChallengeGenerator m_CodeChallengeGenerator; |
|||
readonly ILogger m_Logger; |
|||
|
|||
readonly string m_WellKnownUrl; |
|||
readonly string m_AnonymousUrl; |
|||
readonly string m_SessionTokenUrl; |
|||
readonly string m_ExternalTokenUrl; |
|||
readonly string m_LinkExternalTokenUrl; |
|||
readonly string m_OAuthUrl; |
|||
readonly string m_OAuthTokenUrl; |
|||
readonly string m_OAuthRevokeTokenUrl; |
|||
|
|||
readonly Dictionary<string, string> m_CommonHeaders; |
|||
|
|||
string m_OAuthClientId; |
|||
string m_SessionChallengeCode; |
|||
|
|||
internal AuthenticationNetworkClient(string authenticationHost, |
|||
string projectId, |
|||
ICodeChallengeGenerator codeChallengeGenerator, |
|||
INetworkingUtilities networkClient, |
|||
ILogger logger) |
|||
{ |
|||
m_NetworkClient = networkClient; |
|||
m_CodeChallengeGenerator = codeChallengeGenerator; |
|||
m_Logger = logger; |
|||
|
|||
m_OAuthClientId = "default"; |
|||
|
|||
m_WellKnownUrl = authenticationHost + k_WellKnownUrlStem; |
|||
m_AnonymousUrl = authenticationHost + k_AnonymousUrlStem; |
|||
m_SessionTokenUrl = authenticationHost + k_SessionTokenUrlStem; |
|||
m_ExternalTokenUrl = authenticationHost + k_ExternalTokenUrlStem; |
|||
m_LinkExternalTokenUrl = authenticationHost + k_LinkExternalTokenUrlStem; |
|||
m_OAuthUrl = authenticationHost + k_OAuthUrlStem; |
|||
m_OAuthTokenUrl = authenticationHost + k_OAuthTokenUrlStem; |
|||
m_OAuthRevokeTokenUrl = authenticationHost + k_OauthRevokeStem; |
|||
|
|||
m_CommonHeaders = new Dictionary<string, string> |
|||
{ |
|||
["ProjectId"] = projectId, |
|||
// The Error-Version header enables RFC7807HttpError error responses
|
|||
["Error-Version"] = "v1" |
|||
}; |
|||
} |
|||
|
|||
public IWebRequest<WellKnownKeys> GetWellKnownKeys() |
|||
{ |
|||
return m_NetworkClient.Get<WellKnownKeys>(m_WellKnownUrl); |
|||
} |
|||
|
|||
public void SetOAuthClient(string oAuthClientId) |
|||
{ |
|||
m_OAuthClientId = oAuthClientId; |
|||
} |
|||
|
|||
public IWebRequest<SignInResponse> SignInAnonymously() |
|||
{ |
|||
return m_NetworkClient.Post<SignInResponse>(m_AnonymousUrl, m_CommonHeaders); |
|||
} |
|||
|
|||
public IWebRequest<SignInResponse> SignInWithSessionToken(string token) |
|||
{ |
|||
return m_NetworkClient.PostJson<SignInResponse>(m_SessionTokenUrl, new SessionTokenRequest |
|||
{ |
|||
SessionToken = token |
|||
}, m_CommonHeaders); |
|||
} |
|||
|
|||
public IWebRequest<SignInResponse> SignInWithExternalToken(ExternalTokenRequest externalToken) |
|||
{ |
|||
return m_NetworkClient.PostJson<SignInResponse>(m_ExternalTokenUrl, externalToken, m_CommonHeaders); |
|||
} |
|||
|
|||
public IWebRequest<SignInResponse> LinkWithExternalToken(string accessToken, ExternalTokenRequest externalToken) |
|||
{ |
|||
return m_NetworkClient.PostJson<SignInResponse>(m_LinkExternalTokenUrl, externalToken, WithAccessToken(accessToken)); |
|||
} |
|||
|
|||
public IWebRequest<OAuthAuthCodeResponse> RequestAuthCode(string idToken) |
|||
{ |
|||
m_SessionChallengeCode = m_CodeChallengeGenerator.GenerateCode(); |
|||
|
|||
var payload = $"client_id={m_OAuthClientId}&" + |
|||
$"response_type={k_AuthResponseType}&" + |
|||
$"id_token={idToken}&" + |
|||
$"state={m_CodeChallengeGenerator.GenerateStateString()}&" + |
|||
$"scope={k_OAuthScope}&" + |
|||
$"code_challenge={S256EncodeChallenge(m_SessionChallengeCode)}&" + |
|||
$"code_challenge_method={k_ChallengeMethod}"; |
|||
|
|||
return m_NetworkClient.PostForm<OAuthAuthCodeResponse>(m_OAuthUrl, payload, m_CommonHeaders); |
|||
} |
|||
|
|||
string S256EncodeChallenge(string code) |
|||
{ |
|||
using (var sha256 = SHA256.Create()) |
|||
{ |
|||
var codeVerifierBytes = Encoding.UTF8.GetBytes(code); |
|||
var codeVerifierHash = sha256.ComputeHash(codeVerifierBytes); |
|||
return UrlSafeBase64Encode(codeVerifierHash); |
|||
} |
|||
} |
|||
|
|||
string UrlSafeBase64Encode(byte[] input) |
|||
{ |
|||
return Convert.ToBase64String(input) |
|||
.Replace('+', '-') |
|||
.Replace('/', '_') |
|||
.Replace("=", ""); |
|||
} |
|||
|
|||
public string ExtractAuthCode(IWebRequest<OAuthAuthCodeResponse> authCodeRequest) |
|||
{ |
|||
try |
|||
{ |
|||
var locationUri = new Uri(authCodeRequest.ResponseHeaders["Location"]); |
|||
return ExtractAuthCode(locationUri.ToString(), locationUri.Query); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
m_Logger.Error("Failed to extract auth code. " + ex); |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
string ExtractAuthCode(string locationUri, string query) |
|||
{ |
|||
var queryParams = HttpUtilities.ParseQueryString(query); |
|||
|
|||
string code; |
|||
if (!queryParams.TryGetValue("code", out code)) |
|||
{ |
|||
m_Logger.Error($"Failed to extract auth code. Query parameter 'code' is not found. Location: {locationUri}"); |
|||
} |
|||
|
|||
return code; |
|||
} |
|||
|
|||
public IWebRequest<OAuthTokenResponse> RequestOAuthToken(string authCode) |
|||
{ |
|||
var payload = $"client_id={m_OAuthClientId}" + |
|||
"&grant_type=authorization_code" + |
|||
$"&code_verifier={m_SessionChallengeCode}" + |
|||
$"&code={authCode}"; |
|||
|
|||
return m_NetworkClient.PostForm<OAuthTokenResponse>(m_OAuthTokenUrl, payload, m_CommonHeaders); |
|||
} |
|||
|
|||
public IWebRequest<OAuthTokenResponse> RefreshOAuthToken(string refreshToken) |
|||
{ |
|||
var payload = "grant_type=refresh_token" + |
|||
$"&client_id={m_OAuthClientId}" + |
|||
$"&refresh_token={refreshToken}"; |
|||
|
|||
return m_NetworkClient.PostForm<OAuthTokenResponse>(m_OAuthTokenUrl, payload, m_CommonHeaders, 5); |
|||
} |
|||
|
|||
public IWebRequest<OAuthTokenResponse> RevokeOAuthToken(string accessToken) |
|||
{ |
|||
var payload = $"client_id={m_OAuthClientId}&token={accessToken}"; |
|||
|
|||
return m_NetworkClient.PostForm<OAuthTokenResponse>(m_OAuthRevokeTokenUrl, payload, m_CommonHeaders); |
|||
} |
|||
|
|||
public string ExtractAccessToken(IWebRequest<OAuthTokenResponse> authCodeRequest) |
|||
{ |
|||
return authCodeRequest.ResponseBody.AccessToken; |
|||
} |
|||
|
|||
Dictionary<string, string> WithAccessToken(string accessToken) |
|||
{ |
|||
return new Dictionary<string, string>(m_CommonHeaders) { ["Authorization"] = "Bearer " + accessToken }; |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: cb4a26d0e0091194581e3117bbf91a6a |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using System; |
|||
using Unity.Services.Authentication.Utilities; |
|||
using UnityEngine; |
|||
using Logger = Unity.Services.Authentication.Utilities.Logger; |
|||
|
|||
namespace Unity.Services.Authentication |
|||
{ |
|||
public class AuthenticationService |
|||
{ |
|||
public static IAuthenticationService Instance { get; internal set; } |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 59bf095b5a6ab49ebb2364fbda09f975 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using System; |
|||
using System.Runtime.CompilerServices; |
|||
using System.Threading.Tasks; |
|||
using Newtonsoft.Json; |
|||
using Unity.Services.Authentication.Models; |
|||
using Unity.Services.Authentication.Utilities; |
|||
using Unity.Services.Core; |
|||
|
|||
[assembly: InternalsVisibleTo("Unity.Services.Authentication.Tests")] |
|||
[assembly: InternalsVisibleTo("Unity.Services.Authentication.EditorTests")] |
|||
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // For Moq
|
|||
|
|||
namespace Unity.Services.Authentication |
|||
{ |
|||
enum AuthenticationState |
|||
{ |
|||
SignedOut, |
|||
|
|||
SigningIn, |
|||
|
|||
VerifyingAccessToken, |
|||
|
|||
Authorized, |
|||
Refreshing, |
|||
Expired |
|||
} |
|||
|
|||
class AuthenticationServiceInternal : IAuthenticationService |
|||
{ |
|||
const string k_CacheKeySessionToken = "session_token"; |
|||
|
|||
const string k_IdProviderApple = "apple.com"; |
|||
const string k_IdProviderGoogle = "google.com"; |
|||
const string k_IdProviderFacebook = "facebook.com"; |
|||
const string k_IdProviderSteam = "steampowered.com"; |
|||
|
|||
// NOTE: the REFRESH buffer should always have a larger value than the EXPIRY buffer,
|
|||
// i.e. it happens much earlier than the expiry time. The difference should be large
|
|||
// enough that the refresh process can be attempted (and retried) and succeed (or not)
|
|||
// before the token is actually considered expired.
|
|||
|
|||
/// <summary>
|
|||
/// The buffer time in seconds to start access token refresh before the access token expires.
|
|||
/// </summary>
|
|||
const int k_AccessTokenRefreshBuffer = 120; |
|||
|
|||
/// <summary>
|
|||
/// The buffer time in seconds to treat token as expired before the token's expiry time.
|
|||
/// This is to deal with the time difference between the client and server.
|
|||
/// </summary>
|
|||
const int k_AccessTokenExpiryBuffer = 20; |
|||
|
|||
/// <summary>
|
|||
/// The time in seconds between access token refresh retries.
|
|||
/// </summary>
|
|||
const int k_ExpiredRefreshAttemptFrequency = 300; |
|||
|
|||
/// <summary>
|
|||
/// The max retries to get well known keys from server.
|
|||
/// </summary>
|
|||
const int k_WellKnownKeysMaxAttempt = 3; |
|||
|
|||
readonly IAuthenticationNetworkClient m_NetworkClient; |
|||
readonly IJwtDecoder m_JwtDecoder; |
|||
readonly ICache m_Cache; |
|||
readonly IScheduler m_Scheduler; |
|||
readonly IDateTimeWrapper m_DateTime; |
|||
readonly ILogger m_Logger; |
|||
|
|||
IWebRequest<SignInResponse> m_DelayedTokenRequest; |
|||
string m_PlayerId; |
|||
|
|||
DateTime m_AccessTokenExpiryTime; |
|||
|
|||
internal event Action<AuthenticationState, AuthenticationState> StateChanged; |
|||
|
|||
public event Action<AuthenticationException> SignInFailed; |
|||
public event Action SignedIn; |
|||
public event Action SignedOut; |
|||
|
|||
public bool IsSignedIn => |
|||
State == AuthenticationState.Authorized || |
|||
State == AuthenticationState.Refreshing || |
|||
State == AuthenticationState.Expired; |
|||
|
|||
public bool IsAuthorized => |
|||
State == AuthenticationState.Authorized || |
|||
State == AuthenticationState.Refreshing; |
|||
|
|||
public string AccessToken { get; private set; } |
|||
|
|||
public string PlayerId => |
|||
|
|||
// NOTE: player ID comes in with the authentication request, before the player has actually completed
|
|||
// the authorization process and signed in properly -- so make sure we don't accidentally expose the
|
|||
// player ID too early.
|
|||
IsSignedIn ? m_PlayerId : null; |
|||
|
|||
internal AuthenticationState State { get; set; } |
|||
internal WellKnownKeys WellKnownKeys { get; private set; } |
|||
|
|||
internal AuthenticationServiceInternal(IAuthenticationNetworkClient networkClient, |
|||
IJwtDecoder jwtDecoder, |
|||
ICache cache, |
|||
IScheduler scheduler, |
|||
IDateTimeWrapper dateTime, |
|||
ILogger logger) |
|||
{ |
|||
m_NetworkClient = networkClient; |
|||
m_JwtDecoder = jwtDecoder; |
|||
m_Cache = cache; |
|||
m_Scheduler = scheduler; |
|||
m_DateTime = dateTime; |
|||
m_Logger = logger; |
|||
|
|||
State = AuthenticationState.SignedOut; |
|||
} |
|||
|
|||
void GetWellKnownKeys(AuthenticationAsyncOperation asyncOperation, int attempt) |
|||
{ |
|||
if (WellKnownKeys == null) |
|||
{ |
|||
var wellKnownKeysRequest = m_NetworkClient.GetWellKnownKeys(); |
|||
wellKnownKeysRequest.Completed += resp => WellKnownKeysReceived(asyncOperation, resp, attempt); |
|||
} |
|||
} |
|||
|
|||
internal void WellKnownKeysReceived(AuthenticationAsyncOperation asyncOperation, IWebRequest<WellKnownKeys> response, int attempt) |
|||
{ |
|||
try |
|||
{ |
|||
if (response.RequestFailed) |
|||
{ |
|||
if (attempt < k_WellKnownKeysMaxAttempt) |
|||
{ |
|||
m_Logger.Warning($"Well-known keys request failed (attempt: {attempt}): {response.ResponseCode}, {response.ErrorMessage}"); |
|||
GetWellKnownKeys(asyncOperation, attempt + 1); |
|||
} |
|||
else |
|||
{ |
|||
HandleServerError(asyncOperation, response); |
|||
} |
|||
|
|||
return; |
|||
} |
|||
|
|||
m_Logger.Info("Well-known keys have arrived!"); |
|||
WellKnownKeys = response.ResponseBody; |
|||
|
|||
if (State == AuthenticationState.VerifyingAccessToken) |
|||
{ |
|||
CompleteSignIn(asyncOperation, m_DelayedTokenRequest.ResponseBody); |
|||
} |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
asyncOperation.Fail(AuthenticationError.UnknownError, "Unknown error receiving well-known keys response.", e); |
|||
} |
|||
} |
|||
|
|||
public Task SignInAnonymouslyAsync() |
|||
{ |
|||
if (State == AuthenticationState.SignedOut) |
|||
{ |
|||
if (!string.IsNullOrEmpty(ReadSessionToken())) |
|||
{ |
|||
return SignInWithSessionTokenAsync(); |
|||
} |
|||
|
|||
// I am a first-time anonymous user.
|
|||
return StartSigningIn(m_NetworkClient.SignInAnonymously()).AsTask(); |
|||
} |
|||
|
|||
return AlreadySignedInError().AsTask(); |
|||
} |
|||
|
|||
public Task SignInWithAppleAsync(string idToken) |
|||
{ |
|||
return SignInWithExternalToken(new ExternalTokenRequest |
|||
{ |
|||
IdProvider = k_IdProviderApple, |
|||
Token = idToken |
|||
}).AsTask(); |
|||
} |
|||
|
|||
public Task LinkWithAppleAsync(string idToken) |
|||
{ |
|||
return LinkWithExternalToken(new ExternalTokenRequest |
|||
{ |
|||
IdProvider = k_IdProviderApple, |
|||
Token = idToken |
|||
}).AsTask(); |
|||
} |
|||
|
|||
public Task SignInWithGoogleAsync(string idToken) |
|||
{ |
|||
return SignInWithExternalToken(new ExternalTokenRequest |
|||
{ |
|||
IdProvider = k_IdProviderGoogle, |
|||
Token = idToken |
|||
}).AsTask(); |
|||
} |
|||
|
|||
public Task LinkWithGoogleAsync(string idToken) |
|||
{ |
|||
return LinkWithExternalToken(new ExternalTokenRequest |
|||
{ |
|||
IdProvider = k_IdProviderGoogle, |
|||
Token = idToken |
|||
}).AsTask(); |
|||
} |
|||
|
|||
public Task SignInWithFacebookAsync(string accessToken) |
|||
{ |
|||
return SignInWithExternalToken(new ExternalTokenRequest |
|||
{ |
|||
IdProvider = k_IdProviderFacebook, |
|||
Token = accessToken |
|||
}).AsTask(); |
|||
} |
|||
|
|||
public Task LinkWithFacebookAsync(string accessToken) |
|||
{ |
|||
return LinkWithExternalToken(new ExternalTokenRequest |
|||
{ |
|||
IdProvider = k_IdProviderFacebook, |
|||
Token = accessToken |
|||
}).AsTask(); |
|||
} |
|||
|
|||
public Task SignInWithSteamAsync(string sessionTicket) |
|||
{ |
|||
return SignInWithExternalToken(new ExternalTokenRequest |
|||
{ |
|||
IdProvider = k_IdProviderSteam, |
|||
Token = sessionTicket |
|||
}).AsTask(); |
|||
} |
|||
|
|||
public Task LinkWithSteamAsync(string sessionTicket) |
|||
{ |
|||
return LinkWithExternalToken(new ExternalTokenRequest |
|||
{ |
|||
IdProvider = k_IdProviderSteam, |
|||
Token = sessionTicket |
|||
}).AsTask(); |
|||
} |
|||
|
|||
internal IAsyncOperation SignInWithExternalToken(ExternalTokenRequest externalToken) |
|||
{ |
|||
if (State == AuthenticationState.SignedOut) |
|||
{ |
|||
return StartSigningIn(m_NetworkClient.SignInWithExternalToken(externalToken)); |
|||
} |
|||
|
|||
return AlreadySignedInError(); |
|||
} |
|||
|
|||
internal IAsyncOperation LinkWithExternalToken(ExternalTokenRequest externalToken) |
|||
{ |
|||
var operation = new AuthenticationAsyncOperation(m_Logger); |
|||
if (IsAuthorized) |
|||
{ |
|||
var linkResult = m_NetworkClient.LinkWithExternalToken(AccessToken, externalToken); |
|||
linkResult.Completed += request => LinkResponseReceived(operation, request); |
|||
} |
|||
else |
|||
{ |
|||
m_Logger.Warning("The player is not authorized. Wait until authorized before attempting to link."); |
|||
operation.Fail(AuthenticationError.ClientInvalidUserState); |
|||
} |
|||
|
|||
return operation; |
|||
} |
|||
|
|||
public Task SignInWithSessionTokenAsync() |
|||
{ |
|||
return SignInWithSessionToken(false).AsTask(); |
|||
} |
|||
|
|||
internal IAsyncOperation SignInWithSessionToken(bool isRefreshRequest) |
|||
{ |
|||
var sessionToken = ReadSessionToken(); |
|||
if (State == AuthenticationState.SignedOut || isRefreshRequest) |
|||
{ |
|||
if (string.IsNullOrEmpty(sessionToken)) |
|||
{ |
|||
return SessionTokenNotExistsError(); |
|||
} |
|||
|
|||
m_Logger.Info("Continuing existing session with cached token."); |
|||
|
|||
return StartSigningIn(m_NetworkClient.SignInWithSessionToken(sessionToken), isRefreshRequest); |
|||
} |
|||
|
|||
return AlreadySignedInError(); |
|||
} |
|||
|
|||
internal string ReadSessionToken() |
|||
{ |
|||
return m_Cache.GetString(k_CacheKeySessionToken) ?? string.Empty; |
|||
} |
|||
|
|||
IAsyncOperation StartSigningIn(IWebRequest<SignInResponse> signInRequest, bool isRefreshRequest = false) |
|||
{ |
|||
var asyncOp = CreateSignInAsyncOperation(); |
|||
|
|||
if (!isRefreshRequest) |
|||
{ |
|||
ChangeState(AuthenticationState.SigningIn); |
|||
} |
|||
|
|||
GetWellKnownKeys(asyncOp, 0); |
|||
|
|||
if (isRefreshRequest) |
|||
{ |
|||
signInRequest.Completed += request => RefreshResponseReceived(request); |
|||
} |
|||
else |
|||
{ |
|||
signInRequest.Completed += request => SignInResponseReceived(asyncOp, request); |
|||
} |
|||
|
|||
return asyncOp; |
|||
} |
|||
|
|||
public void SignOut() |
|||
{ |
|||
if (State != AuthenticationState.SignedOut) |
|||
{ |
|||
AccessToken = null; |
|||
m_AccessTokenExpiryTime = default; |
|||
m_PlayerId = null; |
|||
|
|||
m_Cache.DeleteKey(k_CacheKeySessionToken); |
|||
m_Scheduler.CancelAction(ScheduledRefresh); |
|||
|
|||
ChangeState(AuthenticationState.SignedOut); |
|||
} |
|||
} |
|||
|
|||
internal void SignInResponseReceived(AuthenticationAsyncOperation operation, IWebRequest<SignInResponse> request) |
|||
{ |
|||
try |
|||
{ |
|||
if (HandleServerError(operation, request)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
if (WellKnownKeys == null) |
|||
{ |
|||
m_Logger.Info("Well-known keys have not arrived yet, waiting on them to complete sign-in."); |
|||
|
|||
m_DelayedTokenRequest = request; |
|||
|
|||
ChangeState(AuthenticationState.VerifyingAccessToken); |
|||
// operation will complete in WellKnownKeysReceived()
|
|||
} |
|||
else |
|||
{ |
|||
CompleteSignIn(operation, request.ResponseBody); |
|||
} |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
operation.Fail(AuthenticationError.UnknownError, "Unknown error receiving SignIn response.", e); |
|||
} |
|||
} |
|||
|
|||
internal void LinkResponseReceived(AuthenticationAsyncOperation operation, IWebRequest<SignInResponse> request) |
|||
{ |
|||
try |
|||
{ |
|||
if (HandleServerError(operation, request)) |
|||
{ |
|||
return; |
|||
} |
|||
// The data in the response of link can be discarded. We already have all information in current context.
|
|||
// Just mark it as succeed to notify caller that the user is linked successfully.
|
|||
operation.Succeed(); |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
operation.Fail(AuthenticationError.UnknownError, "Unknown error receiving link response.", e); |
|||
} |
|||
} |
|||
|
|||
void CompleteSignIn(AuthenticationAsyncOperation operation, SignInResponse response) |
|||
{ |
|||
try |
|||
{ |
|||
var accessTokenDecoded = m_JwtDecoder.Decode<AccessToken>(response.IdToken, WellKnownKeys); |
|||
if (accessTokenDecoded == null) |
|||
{ |
|||
operation.Fail(AuthenticationError.InvalidAccessToken, "Failed to decode and verify access token."); |
|||
} |
|||
else |
|||
{ |
|||
AccessToken = response.IdToken; |
|||
m_PlayerId = response.UserId; |
|||
m_Cache.SetString(k_CacheKeySessionToken, response.SessionToken); |
|||
|
|||
var expiry = response.ExpiresIn > k_AccessTokenRefreshBuffer ? response.ExpiresIn - k_AccessTokenRefreshBuffer : response.ExpiresIn; |
|||
|
|||
m_AccessTokenExpiryTime = m_DateTime.UtcNow.AddSeconds(response.ExpiresIn - k_AccessTokenExpiryBuffer); |
|||
|
|||
m_Scheduler.ScheduleAction(ScheduledRefresh, expiry); |
|||
|
|||
ChangeState(AuthenticationState.Authorized); |
|||
|
|||
operation.Succeed(); |
|||
} |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
operation.Fail(AuthenticationError.UnknownError, "Unknown error completing sign-in.", e); |
|||
} |
|||
} |
|||
|
|||
internal void ScheduledRefresh() |
|||
{ |
|||
// If we have just been unpaused, Unity's execution order may mean that this triggers
|
|||
// the refresh process when in fact the Expiry process should take hold (i.e. the scheduler executes
|
|||
// this action before the OnApplicationPause callback). So to ensure we don't double the refresh
|
|||
// request, check the token hasn't expired before triggering refresh.
|
|||
if (m_AccessTokenExpiryTime > m_DateTime.UtcNow) |
|||
{ |
|||
RefreshAccessToken(); |
|||
} |
|||
} |
|||
|
|||
internal void RefreshAccessToken() |
|||
{ |
|||
if (IsSignedIn) |
|||
{ |
|||
m_Logger.Info("Making token refresh request..."); |
|||
|
|||
if (State != AuthenticationState.Expired) |
|||
{ |
|||
ChangeState(AuthenticationState.Refreshing); |
|||
} |
|||
|
|||
SignInWithSessionToken(true); |
|||
} |
|||
} |
|||
|
|||
internal void RefreshResponseReceived(IWebRequest<SignInResponse> request) |
|||
{ |
|||
if (request.RequestFailed) |
|||
{ |
|||
m_Logger.Error($"Request failed: {request.ResponseCode}, {request.ErrorMessage}"); |
|||
|
|||
// NOTE: depending on how long it took to fail, this might occur before the access token has _actually_ expired.
|
|||
// This is likely safer than risking an expired access token reaching a consuming service.
|
|||
|
|||
if (request.ServerError && request.ResponseCode < 500) |
|||
{ |
|||
m_Logger.Warning("Failed to refresh access token due to server error, signing out."); |
|||
SignOut(); |
|||
} |
|||
else |
|||
{ |
|||
m_Logger.Warning("Failed to refresh access token due to network error or internal server error, will retry later."); |
|||
|
|||
m_Scheduler.ScheduleAction(RefreshAccessToken, k_ExpiredRefreshAttemptFrequency); |
|||
|
|||
if (State != AuthenticationState.Expired) |
|||
{ |
|||
Expire(); |
|||
} |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
var asyncOp = new AuthenticationAsyncOperation(m_Logger); |
|||
CompleteSignIn(asyncOp, request.ResponseBody); |
|||
if (asyncOp.Status == AsyncOperationStatus.Succeeded) |
|||
{ |
|||
m_Logger.Info("Refresh complete!"); |
|||
} |
|||
else |
|||
{ |
|||
m_Logger.Warning("The access token is not valid. Retry JWKS and refresh again."); |
|||
|
|||
// Refresh failed since we received a bad token. Retry.
|
|||
m_Scheduler.ScheduleAction(RefreshAccessToken, k_ExpiredRefreshAttemptFrequency); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public void ApplicationUnpaused() |
|||
{ |
|||
if (IsAuthorized && |
|||
m_DateTime.UtcNow > m_AccessTokenExpiryTime) |
|||
{ |
|||
m_Logger.Info("Application unpause found the access token to have expired already."); |
|||
Expire(); |
|||
RefreshAccessToken(); |
|||
} |
|||
} |
|||
|
|||
void Expire() |
|||
{ |
|||
AccessToken = null; |
|||
m_AccessTokenExpiryTime = default; |
|||
|
|||
ChangeState(AuthenticationState.Expired); |
|||
} |
|||
|
|||
void ChangeState(AuthenticationState newState) |
|||
{ |
|||
// NOTE: always call this at the end of a method where state is changed, so that any consumer
|
|||
// that has subscribed to the event will get the correct data for the new state.
|
|||
|
|||
m_Logger.Info($"Moved from state [{State}] to [{newState}]"); |
|||
|
|||
var oldState = State; |
|||
State = newState; |
|||
|
|||
HandleStateChanged(oldState, newState); |
|||
} |
|||
|
|||
void HandleStateChanged(AuthenticationState oldState, AuthenticationState newState) |
|||
{ |
|||
StateChanged?.Invoke(oldState, newState); |
|||
switch (newState) |
|||
{ |
|||
case AuthenticationState.Authorized: |
|||
if (oldState != AuthenticationState.Refreshing && |
|||
oldState != AuthenticationState.Expired) |
|||
{ |
|||
SignedIn?.Invoke(); |
|||
} |
|||
|
|||
break; |
|||
case AuthenticationState.SignedOut: |
|||
SignedOut?.Invoke(); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Create <see cref="IAsyncOperation{T}"/> that always fail with <c>ClientInvalidUserState</c> error
|
|||
/// when the user is calling <c>SignIn*</c> methods while not in <c>SignedOut</c> state.
|
|||
/// </summary>
|
|||
/// <returns>The exception that represents the error.</returns>
|
|||
IAsyncOperation AlreadySignedInError() |
|||
{ |
|||
var exception = new AuthenticationException( |
|||
AuthenticationError.ClientInvalidUserState, |
|||
"The player is already signed in. Sign out before attempting to sign in again."); |
|||
m_Logger.Warning(exception.Message); |
|||
|
|||
var asyncOp = new AuthenticationAsyncOperation(null); |
|||
asyncOp.Fail(exception); |
|||
return asyncOp; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Create <see cref="IAsyncOperation{T}"/> that always fail with <c>ClientNoActiveSession</c> error
|
|||
/// when the user is calling <c>SignInWithSessionToken</c> methods while there is no session token stored.
|
|||
/// </summary>
|
|||
/// <returns>The exception that represents the error.</returns>
|
|||
IAsyncOperation SessionTokenNotExistsError() |
|||
{ |
|||
var exception = new AuthenticationException( |
|||
AuthenticationError.ClientNoActiveSession, |
|||
"There is no cached session token."); |
|||
m_Logger.Warning(exception.Message); |
|||
|
|||
// At this point, the contents of the cache are invalid, and we don't want future
|
|||
// SignInAnonymously or SignInWithSessionToken to read the current contents of the key.
|
|||
m_Cache.DeleteKey(k_CacheKeySessionToken); |
|||
|
|||
var asyncOp = new AuthenticationAsyncOperation(null); |
|||
asyncOp.Fail(exception); |
|||
return asyncOp; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Handles the error from server. If request is not failed, do nothing. Otherwise try to parse the error
|
|||
/// and call <c>operation.Fail</c>. Caller shall check <c>operation.Status</c> before moving forward.
|
|||
/// </summary>
|
|||
/// <param name="operation">The async operation to mark failure in case of server error.</param>
|
|||
/// <param name="request">The web request to parse error.</param>
|
|||
/// <typeparam name="T">The type parameter of web request. In case of an error it is not used.</typeparam>
|
|||
/// <returns>Whether there is an error occurred.</returns>
|
|||
bool HandleServerError<T>(AuthenticationAsyncOperation operation, IWebRequest<T> request) |
|||
{ |
|||
if (!request.RequestFailed) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
m_Logger.Error($"Request failed: {request.ResponseCode}, {request.ErrorMessage}"); |
|||
|
|||
if (request.NetworkError) |
|||
{ |
|||
operation.Fail(AuthenticationError.NetworkError); |
|||
return true; |
|||
} |
|||
|
|||
// otherwise it's a server error. Try to parse the error.
|
|||
try |
|||
{ |
|||
var errorResponse = JsonConvert.DeserializeObject<AuthenticationErrorResponse>(request.ErrorMessage); |
|||
operation.Fail(errorResponse.Title, errorResponse.Detail); |
|||
} |
|||
catch (JsonException ex) |
|||
{ |
|||
operation.Fail( |
|||
AuthenticationError.UnknownError, |
|||
"Failed to deserialize server response.", |
|||
ex); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
operation.Fail( |
|||
AuthenticationError.UnknownError, |
|||
"Unknown error deserializing server response. ", |
|||
ex); |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Create the AsyncOperation that will sign-out in case of failure.
|
|||
/// </summary>
|
|||
internal AuthenticationAsyncOperation CreateSignInAsyncOperation() |
|||
{ |
|||
var asyncOp = new AuthenticationAsyncOperation(m_Logger); |
|||
asyncOp.BeforeFail += SendSignInFailedEvent; |
|||
|
|||
return asyncOp; |
|||
} |
|||
|
|||
void SendSignInFailedEvent(AuthenticationAsyncOperation operation) |
|||
{ |
|||
SignInFailed?.Invoke(operation.Exception); |
|||
SignOut(); |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 5a4bdb16b51b6412dbd984e55e006a74 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using System; |
|||
using System.Security.Cryptography; |
|||
using System.Text; |
|||
|
|||
namespace Unity.Services.Authentication |
|||
{ |
|||
interface ICodeChallengeGenerator |
|||
{ |
|||
string GenerateCode(); |
|||
string GenerateStateString(); |
|||
} |
|||
|
|||
class CodeChallengeGenerator : ICodeChallengeGenerator |
|||
{ |
|||
internal const int k_CodeLength = 128; |
|||
|
|||
const string k_CodeChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; |
|||
|
|||
readonly StringBuilder m_CodeBuilder; |
|||
|
|||
internal CodeChallengeGenerator() |
|||
{ |
|||
m_CodeBuilder = new StringBuilder(k_CodeLength); |
|||
} |
|||
|
|||
public string GenerateCode() |
|||
{ |
|||
var randomBytes = new byte[k_CodeLength]; |
|||
using (var randomNumberGenerator = new RNGCryptoServiceProvider()) |
|||
{ |
|||
randomNumberGenerator.GetBytes(randomBytes); |
|||
} |
|||
|
|||
m_CodeBuilder.Clear(); |
|||
for (var i = 0; i < k_CodeLength; i++) |
|||
{ |
|||
m_CodeBuilder.Append(k_CodeChars[randomBytes[i] % k_CodeChars.Length]); |
|||
} |
|||
|
|||
return m_CodeBuilder.ToString(); |
|||
} |
|||
|
|||
public string GenerateStateString() |
|||
{ |
|||
return Guid.NewGuid().ToString(); |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 069948c258b7aa546b3f2841139f3e73 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
fileFormatVersion: 2 |
|||
guid: 020c5be1b3461430e835bf14328b694b |
|||
folderAsset: yes |
|||
DefaultImporter: |
|||
externalObjects: {} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using System; |
|||
|
|||
namespace Unity.Services.Authentication |
|||
{ |
|||
class AccessTokenComponent : IAccessToken |
|||
{ |
|||
IAuthenticationService m_AuthenticationService; |
|||
|
|||
public AccessTokenComponent(IAuthenticationService service) |
|||
{ |
|||
m_AuthenticationService = service; |
|||
} |
|||
|
|||
public string AccessToken => m_AuthenticationService.AccessToken; |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 75db2070b8d34f7a96df15894bf72d5a |
|||
timeCreated: 1615387053 |
|
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Unity.Services.Authentication.Utilities; |
|||
using Unity.Services.Core; |
|||
using UnityEngine; |
|||
using Logger = Unity.Services.Authentication.Utilities.Logger; |
|||
|
|||
namespace Unity.Services.Authentication |
|||
{ |
|||
class AuthenticationPackageInitializer : IInitializablePackage |
|||
{ |
|||
#if AUTHENTICATION_TESTING_STAGING_UAS
|
|||
const string k_UasHost = "https://api.stg.identity.corp.unity3d.com"; |
|||
#else
|
|||
const string k_UasHost = "https://api.prd.identity.corp.unity3d.com"; |
|||
#endif
|
|||
|
|||
public Task Initialize(CoreRegistry registry) |
|||
{ |
|||
var logger = new Logger("[Authentication]"); |
|||
|
|||
var dateTime = new DateTimeWrapper(); |
|||
var networkUtilities = new NetworkingUtilities(Scheduler.Instance, logger); |
|||
var networkClient = new AuthenticationNetworkClient(k_UasHost, |
|||
Application.cloudProjectId, |
|||
new CodeChallengeGenerator(), |
|||
networkUtilities, |
|||
logger); |
|||
var authenticationService = new AuthenticationServiceInternal(networkClient, |
|||
new JwtDecoder(dateTime, logger), |
|||
new PlayerPrefsCache("unity.services.authentication"), |
|||
Scheduler.Instance, |
|||
dateTime, |
|||
logger); |
|||
|
|||
AuthenticationService.Instance = authenticationService; |
|||
registry.RegisterServiceComponent<IPlayerId>(new PlayerIdComponent(authenticationService)); |
|||
registry.RegisterServiceComponent<IAccessToken>(new AccessTokenComponent(authenticationService)); |
|||
|
|||
return Task.CompletedTask; |
|||
} |
|||
|
|||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] |
|||
static void Register() |
|||
{ |
|||
CoreRegistry.Instance.RegisterPackage(new AuthenticationPackageInitializer()) |
|||
.ProvidesComponent<IPlayerId>() |
|||
.ProvidesComponent<IAccessToken>(); |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: b1fe31729a37d4b5a983bec114aa8ac2 |
|||
MonoImporter: |
|||
externalObjects: {} |
|||
serializedVersion: 2 |
|||
defaultReferences: [] |
|||
executionOrder: 0 |
|||
icon: {instanceID: 0} |
|||
userData: |
|||
assetBundleName: |
|||
assetBundleVariant: |
|
|||
using System; |
|||
|
|||
namespace Unity.Services.Authentication |
|||
{ |
|||
class PlayerIdComponent : IPlayerId |
|||
{ |
|||
IAuthenticationService m_AuthenticationService; |
|||
|
|||
public string PlayerId => m_AuthenticationService.PlayerId; |
|||
public event Action<string> PlayerIdChanged; |
|||
|
|||
public PlayerIdComponent(IAuthenticationService service) |
|||
{ |
|||
m_AuthenticationService = service; |
|||
m_AuthenticationService.SignedIn += OnAuthenticationSignedIn; |
|||
m_AuthenticationService.SignedOut += OnAuthenticationSignedOut; |
|||
m_AuthenticationService.SignInFailed += OnAuthenticationSignInFailed; |
|||
} |
|||
|
|||
void OnAuthenticationSignInFailed(AuthenticationException error) |
|||
{ |
|||
NotifyPlayerChanged(); |
|||
} |
|||
|
|||
void OnAuthenticationSignedOut() |
|||
{ |
|||
NotifyPlayerChanged(); |
|||
} |
|||
|
|||
void OnAuthenticationSignedIn() |
|||
{ |
|||
NotifyPlayerChanged(); |
|||
} |
|||
|
|||
void NotifyPlayerChanged() |
|||
{ |
|||
PlayerIdChanged?.Invoke(PlayerId); |
|||
} |
|||
} |
|||
} |
|
|||
fileFormatVersion: 2 |
|||
guid: 5bf960e6acac4f55b28b8015411fd1ff |
|||
timeCreated: 1615387134 |
|
|||
using System; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Unity.Services.Authentication |
|||
{ |
|||
public interface IAuthenticationService |
|||
{ |
|||
bool IsSignedIn { get; } |
|||
string AccessToken { get; } |
|||
string PlayerId { get; } |
|||
|
|||
event Action SignedIn; |
|||
event Action SignedOut; |
|||
event Action<AuthenticationException> SignInFailed; |
|||
|
|||
Task SignInAnonymouslyAsync(); |
|||
Task SignInWithSessionTokenAsync(); |
|||
|
|||
Task SignInWithAppleAsync(string idToken); |
|||
Task LinkWithAppleAsync(string idToken); |
|||
|
|||
Task SignInWithGoogleAsync(string idToken); |
|||
Task LinkWithGoogleAsync(string idToken); |
|||
|
|||
Task SignInWithFacebookAsync(string accessToken); |
|||
Task LinkWithFacebookAsync(string accessToken); |
|||
|
|||
Task SignInWithSteamAsync(string sessionTicket); |
|||
Task LinkWithSteamAsync(string sessionTicket); |
|||
|
|||
void SignOut(); |
|||
|
|||
void ApplicationUnpaused(); |
|||
} |
|||
} |
部分文件因为文件数量过多而无法显示
撰写
预览
正在加载...
取消
保存
Reference in new issue