-
Notifications
You must be signed in to change notification settings - Fork 19
Add Documentation for OpenDID #313
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
77b80cb
add opendid documentation
abdulmth 846f952
remove SIOP related comment
abdulmth 42d7947
Draft review
ChrisChinchilla 8af6141
Review
ChrisChinchilla a568b1d
Update docs/develop/08_opendid/01_overview.md
ChrisChinchilla 748d5a8
Update docs/develop/08_opendid/01_overview.md
ChrisChinchilla 156a8ea
Update docs/develop/08_opendid/04_integrate_opendid.md
ChrisChinchilla 927c5cc
Update docs/develop/08_opendid/04_integrate_opendid.md
ChrisChinchilla b2c0030
Update docs/develop/08_opendid/05_demo_project.md
ChrisChinchilla aa93ade
Update docs/develop/08_opendid/06_advanced.md
ChrisChinchilla 004377a
Update docs/develop/08_opendid/03_opendid_service.md
ChrisChinchilla a76c357
Update docs/develop/08_opendid/03_opendid_service.md
ChrisChinchilla 10cf605
Update docs/develop/08_opendid/04_integrate_opendid.md
ChrisChinchilla 04a3ba7
Changes
ChrisChinchilla 1563bcf
Fixes
ChrisChinchilla fc6f356
Review
ChrisChinchilla File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
--- | ||
id: what-is-opendid | ||
title: Overview | ||
--- | ||
|
||
[OpenDID](https://github.com/KILTprotocol/opendid) is an OpenID Provider implementation capable of authenticating users through their [Decentralized Identifier (DID)](../../concepts/02_did.md) and Verifiable Credentials. | ||
|
||
It follows the [OpenID Connect 1.0 Specification](https://openid.net/specs/openid-connect-core-1_0.html#Introduction) and acts as a bridge between the decentralized identity world and the centralized authentication world supporting both the implicit and Authorization Code Flow. | ||
|
||
A major use of OpenDID is Single Sign-On (SSO), which allows users to use the same DID and credentials to sign into multiple platforms and web services. For instance, by adding a "Sign in with KILT" button to a webpage. | ||
|
||
Although integrating that functionality into a webpage is relatively simple, configuring and running OpenDID is more involved. | ||
|
||
:::info | ||
|
||
To learn more about the flow of OpenDID, see the [OpenDID Flow](./02_opendid_flow.md) documentation. | ||
|
||
::: | ||
|
||
## Project container structure | ||
|
||
The project consist of multiple parts that supplement and interact with each other all shipped as Docker containers and released to Docker Hub. | ||
|
||
### opendid-setup container | ||
|
||
The OpenDID Service needs configuration to run, which you can apply using this | ||
container. | ||
For example, it requires a DID to establish a session with an identity wallet. | ||
This container creates a DID and the necessary configuration by providing an account with enough funds. | ||
|
||
Learn more in the [run setup container documentation](./03_opendid_service.md#run-setup-container). | ||
|
||
### kiltprotocol/opendid container | ||
|
||
This container [runs the OpenDID Service](./03_opendid_service.md#run-the-service), both the OpenDID front and back end. | ||
This container requires the configuration file created from the `opendid-setup` container. | ||
|
||
### kiltprotocol/opendid-demo | ||
|
||
This container is a [web app demo](./05_demo_project.md), including front and back end services to demonstrate the use of OpenDID. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
--- | ||
id: flow | ||
title: OpenDID Flow | ||
--- | ||
|
||
This guide explains the internal workings of OpenDID. | ||
Understanding this flow is helpful for setting up and configuring an OpenDID Service but less important if you only need to integrate it in an application. | ||
|
||
OpenDID includes interactions between multiple apps to authenticate and authorize users. | ||
Common use cases include the following: | ||
|
||
- Web app front end (app that includes the login button, for example, the demo app) | ||
- Web app back end | ||
- OpenDID front end | ||
- OpenDID back end | ||
- Identity wallet that follows [the Credential API spec](https://github.com/KILTprotocol/spec-ext-credential-api) (typically a browser extension, for example, [Sporran](https://www.sporran.org/)) | ||
|
||
The following steps outline the interactions necessary to implement [the implicit flow](https://openid.net/specs/openid-connect-core-1_0.html#ImplicitFlowAuth): | ||
|
||
1. The user clicks the login button on the *web app front end*. | ||
2. The *web app front end* redirects the user to the *OpenDID front end*. | ||
3. The user chooses what wallet to authenticate with. | ||
4. The *OpenDID back end* establishes a secure session with the *identity wallet*. | ||
5. The *OpenDID back end* optionally requests a credential that implements a specific CType. | ||
6. The *identity wallet* provides the *OpenDID back end* with the requested credential, after authenticating the DID holder. | ||
7. The *OpenDID back end* returns a `id_token` as a JSON web token (JWT) to the *OpenDID front end*. | ||
8. *OpenDID front end* redirects the user back to a specific `redirect_url` on the *web app front end* including the `id_token`. | ||
9. The *web app front end* detects the `id_token` and sends it to the *web app back end*. | ||
10. The *web app back end* verifies the `id_token` and ensures the validity of the credential. | ||
|
||
The following sequence diagram summarizes the flow: | ||
|
||
```mermaid | ||
sequenceDiagram | ||
|
||
participant AB as WebApp Backend | ||
participant AF as WebApp Frontend | ||
participant OF as OpenDID Frontend | ||
participant OB as OpenDID Backend | ||
participant IW as Identity Wallet | ||
|
||
AF->>OF: (1, 2) Authorize (redirect_uri: /callback) | ||
OF->>OF: (3) Pick Identity Wallet | ||
critical (4) Key Exchange | ||
OF->>OB: GET Challenge | ||
OB-->>OF: Challenge | ||
OF->>IW: Start Session | ||
IW-->>OF: Encrypted Challenge | ||
OF->>OB: POST Challenge | ||
OB-->>OF: OK | ||
end | ||
|
||
critical Authenticate | ||
OF->>OB: (5) GET Credential Requirements | ||
OB-->>OF: Credential Requirements | ||
OF->>IW: (6) Request Credential | ||
IW->>IW: Authenticate User | ||
IW->>OF: Credential | ||
OF->>OB: POST Credential | ||
OB->>OB: Verify Credential | ||
OB->>OF: (7) `id_token`) | ||
end | ||
|
||
OF->>AF: redirect to /callback with `id_token` | ||
AF->>AB: (8) `id_token` | ||
AB->>AB: (9) verify `id_token` | ||
AB->>AF: (10) Access granted. | ||
|
||
``` | ||
|
||
:::info | ||
Although this example describes the implicit flow, [the authorization code flow](https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth) is similar. | ||
Instead of returning an `id_token` directly, the OpenDID service instead returns a `code` to exchange for an `id_token` using the `token` endpoint. | ||
::: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
--- | ||
id: opendid_service | ||
title: Run OpenDID Service | ||
--- | ||
|
||
<!-- TODO: Overview and steps --> | ||
ChrisChinchilla marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
## Configuration | ||
|
||
Running the OpenDID service requires some configuration and a KILT DID. | ||
The DID establishes a secure session with an identity wallet using a key agreement key of type `X25519KeyAgreementKey2019` included in the DID Document generated by the setup container. | ||
|
||
OpenDID serves a [well-known DID configuration](https://identity.foundation/.well-known/resources/did-configuration/), which the identity wallet uses to ensure that the domain is linked to the specified DID. | ||
|
||
### Run setup container | ||
|
||
Before running the `opendid-setup` container, set two environment variables: | ||
|
||
1. `SEED` to provide an account with funds (minimum of 3 KILT) for the DID generation. | ||
|
||
```bash | ||
export SEED="dont try this seed its completely made up for this nice example" | ||
``` | ||
|
||
2. `ENDPOINT` | ||
|
||
Set to "spiritnet" if the account is on the spiritnet production network. | ||
|
||
```bash | ||
export ENDPOINT="spiritnet" | ||
``` | ||
|
||
Set to "peregrine" if the account is on the peregrine test network. | ||
|
||
```bash | ||
export ENDPOINT="peregrine" | ||
``` | ||
|
||
Then run the setup with the following command: | ||
|
||
```bash | ||
docker run --rm -it -e "ENDPOINT=${ENDPOINT}" -v $(pwd):/data docker.io/kiltprotocol/opendid-setup:latest "${SEED}" | ||
``` | ||
|
||
The command generates a set of new mnemonics and then derives a DID from them and generates multiple files into the current directory: | ||
|
||
1. `config.yaml` The configuration file used by the OpenDID service. | ||
|
||
:::warning | ||
You only need the `config.yaml` to run the OpenDID service. | ||
This file includes the generated mnemonic and secret keys and you should protect it from unauthorized access. | ||
::: | ||
|
||
2. `did-secrets.json` This file contains the public and secret keys in the DID Document. | ||
|
||
:::warning | ||
Keep a secure backup of this file as it contains all the secret keys. | ||
::: | ||
|
||
3. `did-document.json` contains the DID Document generated by this setup. | ||
|
||
The container generates sensible defaults in the `config.yaml` file, but here are some values you might want to change: | ||
|
||
- Set `production` to true, this only allows secure connections. | ||
- Set the `WellKnownDid` > `origin`, which should match the host running the OpenDID service. | ||
- Set the keys used for JWT issuance in the `jwt` section. | ||
- The `client` section, including: | ||
|
||
- The client ID as a key (The default is: `example-client`). | ||
- The `requirements` section, including: | ||
- What CTypes are required for authentication. | ||
- The trusted attesters as an address (The default is for the [SocialKYC attester](https://socialkyc.io/)). | ||
|
||
:::note info | ||
|
||
The generated default `config.yaml` requires an [email credential](https://test.ctypehub.galaniprojects.de/ctype/kilt:ctype:0x3291bb126e33b4862d421bfaa1d2f272e6cdfc4f96658988fbcffea8914bd9ac) issued by an attester. | ||
|
||
::: | ||
|
||
- What `redirect_url`s the service accepts (The default is `http://localhost:1606/callback.html` for the demo project). | ||
- The `clientSecret` is optional but recommended. If you use the authorization code flow, the `token` endpoint requires it. | ||
|
||
## Run the service | ||
|
||
When you've made changes to the `config.yaml` file, you can run the OpenDID service. | ||
|
||
1. Specify the runtime through the `RUNTIME` environment variable: | ||
|
||
Set to `"spiritnet"` for production KILT | ||
|
||
```bash | ||
export RUNTIME="spiritnet" | ||
``` | ||
|
||
Set to `"peregrine"` for the KILT test net. | ||
|
||
```bash | ||
export RUNTIME="peregrine" | ||
``` | ||
|
||
2. Run the `docker.io/kiltprotocol/opendid` docker image. | ||
|
||
```bash | ||
docker run -d --rm \ | ||
-v $(pwd)/config.yaml:/app/config.yaml \ | ||
-v $(pwd)/checks:/app/checks \ | ||
-e "RUNTIME=${RUNTIME}" \ | ||
-p 3001:3001 \ | ||
docker.io/kiltprotocol/opendid:latest | ||
``` | ||
|
||
3. Open the login page at _http://localhost:3001_. | ||
|
||
## Next steps | ||
|
||
With configuration in place and a service running, next you need to [integrate OpenDID into an application](./04_integrate_opendid.md) so that a user can use the login page. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
--- | ||
id: integrate_opendid | ||
title: Integrate OpenDID | ||
--- | ||
|
||
OpenDID follows the [OpenID Connect 1.0 Specification](https://openid.net/specs/openid-connect-core-1_0.html#Introduction) and implements both the [implicit flow](https://openid.net/specs/openid-connect-core-1_0.html#ImplicitFlowSteps) | ||
and the [authorization code flow](https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth). | ||
Read the [demo project guide](05_demo_project.md) for an example of integrating OpenDID. | ||
|
||
## Authorization code flow | ||
|
||
Initiate the flow by redirecting to the **GET** `/api/v1/authorize` endpoint on the OpenDID service and setting the following query URL-encoded parameters: | ||
|
||
- `response_type`: set value to `code` to indicate Authorization Code Flow. | ||
- `client_id`: The client ID set in the `config.yaml` file. | ||
- `redirect_uri`: OpenDID redirects to this URL after authentication. | ||
- `scope`: set value to `openid`. | ||
- `state`: set to a secure random number. | ||
- `nonce`: optional value, set to a secure random number. | ||
|
||
**Example**: | ||
|
||
``` | ||
GET /api/v1/authorize? | ||
response_type=code& | ||
client_id=example-client& | ||
redirect_uri=http://localhost:1606/callback.html& | ||
scope=openid& | ||
state=rkw49cbvd4azu5dsln1xbl& | ||
nonce=vedur4om49ei8w91jt7wt HTTP/1.1 | ||
``` | ||
|
||
After successful authentication, the OpenDID service redirects back to the provided `redirect_uri` with `code` and `state` query parameters. | ||
|
||
**Example**: | ||
|
||
``` | ||
/callback.html? | ||
code=lwDS1ZpQBwR4Vdm53_L8bWpUJ1mx9A0mA_-86dubTqzqzwGazx1RyLX4Z_qf& | ||
state=rkw49cbvd4azu5dsln1xbl | ||
``` | ||
|
||
You can retrieve the `id_token` by calling the **POST** `/api/v1/token` and providing the following values in the form serialization: | ||
|
||
- `code`: code value returned from `authorize`. | ||
- `grant_type`: set value to `authorization_code`. | ||
- `redirect_uri`: the same `redirect_uri` used in `authorize`. | ||
- `client_id`: the client ID set in the `config.yaml` file. | ||
- `client_secret`: the client secret value set in the `config.yaml` file. | ||
|
||
**Example**: | ||
|
||
``` | ||
POST /api/v1/token HTTP/1.1 | ||
Content-Type: application/x-www-form-urlencoded | ||
|
||
code=lwDS1ZpQBwR4Vdm53_L8bWpUJ1mx9A0mA_-86dubTqzqzwGazx1RyLX4Z_qf& | ||
grant_type=authorization_code& | ||
redirect_uri=http%3A%2F%2Flocalhost%3A1606%2Fcallback.html& | ||
client_id=example-client& | ||
client_secret=insecure_client_secret | ||
``` | ||
|
||
ChrisChinchilla marked this conversation as resolved.
Show resolved
Hide resolved
|
||
The OpenDID service returns the `id_token` in the response body serialized as a JSON object. | ||
|
||
```json | ||
{ | ||
"access_token": "SsFhhSBMWsLeDMxVUVGreKARNwYxMZtGFfBr0-ZiH6iondSmwPRvQDqkG6Fh", | ||
"token_type": "bearer", | ||
"refresh_token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkaWQ6a2lsdDo0b0VkNENVV3RwbkxUVnZENVBFd2lMUmlqMWdzQmprS1JMbVpES2lCOEdqN2I2V0wiLCJ3M24iOiJjdXN0b20iLCJleHAiOjE3MTY4MTYwNjQsImlhdCI6MTcxNjgxNTQ2NCwiaXNzIjoiZGlkOmtpbHQ6NHJzQkE3dEQ1S1E4TDlXSGpGallRdUhrTWtha2NmSGRDNUNhUVVjVXh5VWpEVkhBIiwiYXVkIjoiYXV0aGVudGljYXRpb24iLCJwcm8iOnsiRW1haWwiOiJhYmR1bEBraWx0LmlvIn0sIm5vbmNlIjoidmVkdXI0b200OWVpOHc5MWp0N3d0In0.yOmE_9jWKcAu8LpjVx7IsFyOOvlKbgo2oC4Imf-qrLY", | ||
"id_token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkaWQ6a2lsdDo0b0VkNENVV3RwbkxUVnZENVBFd2lMUmlqMWdzQmprS1JMbVpES2lCOEdqN2I2V0wiLCJ3M24iOiJjdXN0b20iLCJleHAiOjE3MTY4MTU1MjQsImlhdCI6MTcxNjgxNTQ2NCwiaXNzIjoiZGlkOmtpbHQ6NHJzQkE3dEQ1S1E4TDlXSGpGallRdUhrTWtha2NmSGRDNUNhUVVjVXh5VWpEVkhBIiwiYXVkIjoiYXBwbGljYXRpb24iLCJwcm8iOnsiRW1haWwiOiJhYmR1bEBraWx0LmlvIn0sIm5vbmNlIjoidmVkdXI0b200OWVpOHc5MWp0N3d0In0.YlRE9EGnSExQCb5m2iy4__58PZJlZdCZMsSvsuW4oj8" | ||
} | ||
``` | ||
|
||
:::note | ||
In full-stack applications, calling the `token` endpoint is usually done through the back end to improve security. | ||
::: | ||
|
||
The `id_token` is a bearer JSON web token (JWT) signed by the JWT key-pair specified in the `config.yaml` file of the OpenDID service. | ||
You must verify this using the JWT public key, for example, by the back end of the Web app. | ||
|
||
## Implicit flow | ||
|
||
Initiate the flow by redirecting to the **GET** `/api/v1/authorize` endpoint on the OpenDID Service and setting the following query parameters: | ||
|
||
- `response_type`: set value to `id_token` to indicate Implicit Flow. | ||
- `client_id`: The client ID set in the config.yaml file. | ||
- `redirect_uri`: OpenDID redirects to this URL after authentication. | ||
- `scope`: set value to `openid`. | ||
- `state`: set to a secure random number. | ||
- `nonce`: optional value, set to a secure random number. | ||
|
||
**Example**: | ||
|
||
``` | ||
GET /api/v1/authorize? | ||
response_type=id_token& | ||
client_id=example-client& | ||
redirect_uri=http://localhost:1606/callback.html& | ||
scope=openid& | ||
state=o0fl4c9gwylymzw5f4ik& | ||
nonce=ia7sa06ungxdfzaqphk2 HTTP/1.1 | ||
``` | ||
|
||
After successful authentication, OpenDID redirects back to the provided `redirect_uri` with `id_token` and `state` | ||
**fragment components**. | ||
|
||
**Example**: | ||
|
||
``` | ||
/callback.html# | ||
id_token=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkaWQ6a2lsdDo0b0VkNENVV3RwbkxUVnZENVBFd2lMUmlqMWdzQmprS1JMbVpES2lCOEdqN2I2V0wiLCJ3M24iOiJjdXN0b20iLCJleHAiOjE3MTY4ODQ5MDYsImlhdCI6MTcxNjg4NDg0NiwiaXNzIjoiZGlkOmtpbHQ6NHJzQkE3dEQ1S1E4TDlXSGpGallRdUhrTWtha2NmSGRDNUNhUVVjVXh5VWpEVkhBIiwiYXVkIjoiYXBwbGljYXRpb24iLCJwcm8iOnsiRW1haWwiOiJhYmR1bEBraWx0LmlvIn0sIm5vbmNlIjoiOTFzN2ZnZDZvcjR3c2NkdGVtcXQifQ.xTy3Oyc5e-vlP10mGy0f9GqNU4LV97s77s-l7w5EwF0& | ||
refresh_token=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJkaWQ6a2lsdDo0b0VkNENVV3RwbkxUVnZENVBFd2lMUmlqMWdzQmprS1JMbVpES2lCOEdqN2I2V0wiLCJ3M24iOiJjdXN0b20iLCJleHAiOjE3MTY4ODU0NDYsImlhdCI6MTcxNjg4NDg0NiwiaXNzIjoiZGlkOmtpbHQ6NHJzQkE3dEQ1S1E4TDlXSGpGallRdUhrTWtha2NmSGRDNUNhUVVjVXh5VWpEVkhBIiwiYXVkIjoiYXV0aGVudGljYXRpb24iLCJwcm8iOnsiRW1haWwiOiJhYmR1bEBraWx0LmlvIn0sIm5vbmNlIjoiOTFzN2ZnZDZvcjR3c2NkdGVtcXQifQ.87UHGid3OotxO8Wpfuw-1sc5fsQJVt5gc2cqp9dVHiw& | ||
state=nitctpl7nmqcpvob7xthrw& | ||
token_type=bearer | ||
``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
--- | ||
id: demo_project | ||
title: Demo Project | ||
--- | ||
|
||
The example code at [demo-project](https://github.com/KILTprotocol/opendid/tree/main/demo-project) contains a minimal application that uses OpenDID. | ||
It's an [express](https://expressjs.com) application that exposes three things: | ||
|
||
- A login page that handles the dispatching of the user to the OpenDID service. | ||
- A callback page for one of the OpenID Connect flows supported to accept the token. | ||
- A protected resource that only authenticated users can access. | ||
|
||
For the demo application to work you need a running OpenDID Service and an identity wallet that follows [the Credential API spec](https://github.com/KILTprotocol/spec-ext-credential-api) (e.g. [Sporran](https://www.sporran.org/)) with a DID and Credential issued by the required attester specified in the `config.yaml` file (Default is SocialKYC). | ||
If you follow the steps in this section in order, you have all the necessary components for the demo application to run. | ||
|
||
Run the pre-configured demo application with the following command: | ||
|
||
```bash | ||
docker run -d -it --rm \ | ||
--name demo-frontend \ | ||
-p 1606:1606 \ | ||
docker.io/kiltprotocol/opendid-demo | ||
``` | ||
|
||
The demo page runs on _http://localhost:1606_. It pre-fills the Client ID value and offers login buttons to follow the implicit or authorization code flow. | ||
|
||
:::note | ||
You can set the JSON web token (JWT) secret can with the `TOKEN_SECRET` environment variable inside the docker container. It must match | ||
the one specified in the `config.yaml` file to correctly verify the `id_token`. The default is `super-secret-jwt-secret`. | ||
::: |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.