Skip to content
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

Add Instagram verification #19

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
104 changes: 104 additions & 0 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,98 @@ When this request is recieved the service does the following:
}
```


## Request Instagram Verification

When this request is made the service stores the `did`, `username`, and a *`timestamp`* in it's database of requested instagram verifications.

Note: due to the OAuth Authorization code flow, the service can provide a convenient HTTP redirection 307 to user by setting the env variable `INSTAGRAM_HTTP_REDIRECT=true`

**Endpoint:** `GET /api/v0/request-instagram`

**Query params:**

```jsx
- did: <user-DID>
- username: <instagram-username>
```

Example `/api/v0/request-instagram?username=<instagram-username>&did=<user-DID>`

**Response:**

With `INSTAGRAM_HTTP_REDIRECT=true`, a redirection to [Instagram Authorization Window](https://developers.facebook.com/docs/instagram-basic-display-api/overview/#authorization-window).

Otherwise:

```jsx
{
"status": "success",
"data": {
"statusCode": 307,
"headers": {
"Location": "https://api.instagram.com/oauth/authorize/?client_id=<INSTAGRAM_CLIENT_ID>&redirect_uri=<INSTAGRAM_REDIRECT_URI>&scope=user_profile&response_type=code&state=<challenge-code>"
},
"body": ""
}
}
```

## Confirm Instagram Verification

When this request is received the service does the following:

1. Validate that the JWS has a correct signature (is signed by the DID in the `kid` property of the JWS)
2. Retrieve the stored request from the database using the DID part of the `kid` if present, otherwise respond with an error
3. Verify that the JWS has content equal to the `challenge-code`, otherwise return error
4. Verify that the *`timestamp`* is from less than 30 minutes ago
5. Call Instagram OAuth API to convert Authorization code to Oauth access token
6. Get Instagram User profile from the Graph API (`/me`) with the previous access token and
7. Verify that the username from Instagram authenticated response is equal to the stored one.
8. Create a Verifiable Credential with the content described below, sign it with the service key (web-did), and send this as the response

**Endpoint:** `POST /api/v0/confirm-instagram`

**Body:**

```jsx
{
code: <code-query-param-string>
jws: <jws-string>
}
```

**Response:**

```jsx
{
status: 'success',
data: {
attestation: <did-jwt-vc-string>
}
}
```

**Verifiable Credential content:**

```jsx
{
sub: <user-DID>,
nbf: 1562950282, // Time jwt was issued
vc: {
'@context': ['https://www.w3.org/2018/credentials/v1'],
type: ['VerifiableCredential'],
credentialSubject: {
account: {
type: 'Instagram',
username: <instagram-username>,
userId: <instagram-userid>
}
}
}
}
```

# User flows

These user flows describe high level user interactions needed to facilitate the verifications. They are mainly meant to illustrate the rough flow so that individual steps that happen in the background can be more easily understood, which is useful if you just want to write a simple test that validates that the services work. The actual user facing implementation can have more optimized UX (e.g. automatically populating a tweet).
Expand Down Expand Up @@ -395,6 +487,18 @@ These user flows describe high level user interactions needed to facilitate the
1. A JWS containing the *challenge code* is created using the js-did library
2. The JWS is sent to the *confirm discourse* endpoint and the Verifiable Credential is returned

## Instagram verification

1. User inputs their instagram username and clicks verify
1. A request with users DID, instagram username is made to the *request instagram* endpoint
2. The returned *challenge code* is temporarily stored
2. User is redirected to [Instagram Authorization window](https://developers.facebook.com/docs/instagram-basic-display-api/overview/#authorization-window) on Instagram's website to login (if not already logged-in) and approve the request of information access.
This can be done by an HTTP redirect if `INSTAGRAM_HTTP_REDIRECT` env var is set or by the verification website.
3. Instagram Auth API redirects to the `INSTAGRAM_REDIRECT_URI` specified in .env and the Instagram Client App settings. The redirection is done with an OAuth2 authorization `code` in query param and `state` containing the *challenge code*.
4. User clicks verify and they now get the Verifiable credential back from the service
1. A JWS containing the *challenge code* is created using the js-did library
2. The JWS and the OAuth2 authorization code are sent to the *confirm isntagram* endpoint and the Verifiable Credential is returned

# Implementation details

Here is a few details that will help with the implementation and in particular related to the DID and JWS stuff. Note that some of these are new libraries so definitely reach out if something seems off!
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

<!-- ALL-CONTRIBUTORS-BADGE:END -->

> Services for issuing verifiable credentials that link a decentralized identifier (DID) to various social accounts including Twitter, Github, and Discord. Additional account types can be added in the future.
> Services for issuing verifiable credentials that link a decentralized identifier (DID) to various social accounts including Twitter, Github, Instagram and Discord. Additional account types can be added in the future.

## What's included

Expand Down
6 changes: 6 additions & 0 deletions packages/server/.template.env
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,9 @@ TWITTER_CONSUMER_SECRET=
TWITTER_ACCESS_TOKEN=
TWITTER_ACCESS_TOKEN_SECRET=
SEGMENT_WRITE_KEY=
INSTAGRAM_CLIENT_ID=
INSTAGRAM_CLIENT_SECRET=
# Redirect URI should match with the registered one in Instagram App on Facebook Developers portal
INSTAGRAM_REDIRECT_URI=
# Choose if you want to be redirected to Instagram Auth on calling GET /api/v0/request-instagram, or if you want a JSON
INSTAGRAM_HTTP_REDIRECT=false
3 changes: 2 additions & 1 deletion packages/server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

<!-- ALL-CONTRIBUTORS-BADGE:END -->

> A decentralized identifier (DID) verification service for Ceramic. Available methods include Twitter, Discord, and Github.
> A decentralized identifier (DID) verification service for Ceramic. Available methods include Twitter, Discord, Instagram and Github.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess discourse should be here as well 😅


## Install

Expand All @@ -27,6 +27,7 @@ Copy `.template.env` to `.env` and update the variables. You'll need the followi
- Ceramic client url to resolve `@ceramicnetwork/3id-did-resolver`
- Twitter developer tokens (you need all 4 items)
- Github account username & API token. "Account Settings" > "Developer settings" > "Personal access tokens"
- Instagram app id & secret from [Facebook Apps](https://developers.facebook.com/apps/) and a registered redirect URI
- Redis database URL & password
- (optional) Segment token

Expand Down
35 changes: 35 additions & 0 deletions packages/server/serverless.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ provider:
TWITTER_CONSUMER_SECRET: ${self:custom.secrets.TWITTER_CONSUMER_SECRET}
TWITTER_ACCESS_TOKEN: ${self:custom.secrets.TWITTER_ACCESS_TOKEN}
TWITTER_ACCESS_TOKEN_SECRET: ${self:custom.secrets.TWITTER_ACCESS_TOKEN_SECRET}
INSTAGRAM_CLIENT_ID: ${self:custom.secrets.INSTAGRAM_CLIENT_ID}
INSTAGRAM_CLIENT_SECRET: ${self:custom.secrets.INSTAGRAM_CLIENT_SECRET}
INSTAGRAM_REDIRECT_URI: ${self:custom.secrets.INSTAGRAM_REDIRECT_URI}
INSTAGRAM_HTTP_REDIRECT: ${self:custom.secrets.INSTAGRAM_HTTP_REDIRECT}

# Enable auto-packing of external modules
custom:
Expand Down Expand Up @@ -116,6 +120,24 @@ functions:
method: post
cors: true
path: /api/v0/request-twitter
request-instagram:
handler: src/api_handler.request_instagram
timeout: 30
vpc:
securityGroupIds:
- Fn::GetAtt: [ ServerlessSecurityGroup, GroupId ]
subnetIds:
- Ref: PrivateSubnetA
events:
- http:
method: get
cors: true
path: /api/v0/request-instagram
request:
parameters:
querystrings:
did: true
username: true
verify-twitter:
handler: src/api_handler.verify_twitter
timeout: 30
Expand All @@ -142,6 +164,19 @@ functions:
method: post
cors: true
path: /api/v0/confirm-discord
verify-instagram:
handler: src/api_handler.verify_instagram
timeout: 30
vpc:
securityGroupIds:
- Fn::GetAtt: [ ServerlessSecurityGroup, GroupId ]
subnetIds:
- Ref: PrivateSubnetA
events:
- http:
method: post
cors: true
path: /api/v0/confirm-instagram

resources:
Resources: ${file(cf-resources.yml)}
Expand Down
26 changes: 25 additions & 1 deletion packages/server/src/__tests__/api_handler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ describe('apiHandler', () => {
TWITTER_CONSUMER_SECRET: 'FAKE',
KEYPAIR_PRIVATE_KEY: '4baba8f4a',
KEYPAIR_PUBLIC_KEY: '04fff936f805ee2',
GITHUB_PERSONAL_ACCESS_TOKEN: 'FAKE'
GITHUB_PERSONAL_ACCESS_TOKEN: 'FAKE',
INSTAGRAM_CLIENT_ID: '123',
INSTAGRAM_CLIENT_SECRET: 'secret',
INSTAGRAM_REDIRECT_URI: 'http://my.url'
}
process.env.SECRETS = secrets
})
Expand Down Expand Up @@ -76,4 +79,25 @@ describe('apiHandler', () => {
done()
})
})

// FIXME fix the "Error: input is invalid type"
test.skip('request instagram', done => {
apiHandler.request_instagram(
{ queryStringParameters: {} },
{},
(err, res) => {
expect(err).toBeNull()
expect(res).not.toBeNull()
done()
}
)
})

test('verify instagram', done => {
apiHandler.verify_instagram({}, {}, (err, res) => {
expect(err).toBeNull()
expect(res).not.toBeNull()
done()
})
})
})
94 changes: 94 additions & 0 deletions packages/server/src/api/__tests__/instagram-request.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
const InstagramRequestHandler = require('../instagram-request')

describe('InstagramRequestHandler', () => {
let sut
let instagramMgrMock = {
generateRedirectionUrl: jest.fn(),
saveRequest: jest.fn()
}
let claimMgrMock = { issue: jest.fn() }
let analyticsMock = { trackRequestInstagram: jest.fn() }

beforeAll(() => {
sut = new InstagramRequestHandler(
instagramMgrMock,
claimMgrMock,
analyticsMock
)
})

test('empty constructor', () => {
expect(sut).not.toBeUndefined()
})

test('no did nor username', done => {
sut.handle({}, {}, (err, res) => {
expect(err).not.toBeNull()
expect(err.code).toEqual(400)
expect(err.message).toEqual('no did nor username')
done()
})
})

test('no did', done => {
sut.handle(
{ queryStringParameters: { username: 'anthony' } },
{},
(err, res) => {
expect(err).not.toBeNull()
expect(err.code).toEqual(403)
expect(err.message).toEqual('no did')
done()
}
)
})

test('no username', done => {
sut.handle(
{
queryStringParameters: { did: 'did:123' }
},
{},
(err, res) => {
expect(err).not.toBeNull()
expect(err.code).toEqual(400)
expect(err.message).toEqual('no username')
done()
}
)
})

test('happy path', done => {
instagramMgrMock.generateRedirectionUrl.mockReturnValue(
'http://some.valid.url'
)
sut.handle(
{
queryStringParameters: { username: 'wallkanda', did: 'did:123' }
},
{},
(_err, res) => {
expect(res).not.toBeNull()
// console.log(res)
expect(res.statusCode).toEqual(307)
expect(res.headers.Location).not.toBeNull()
done()
}
)
})

// test('happy path with HTTP redirect', done => {
// sut.handle(
// {
// queryStringParameters: { username: 'wallkanda', did: 'did:123' }
// },
// {},
// (_err, res) => {
// expect(res).not.toBeNull()
// expect(res.status).toEqual(307)
// expect(res.headers.get('Location')).not.toBeNull()
// done()
// }
// )
// })
})
Loading