Browse Source

feat: improve authentication guide

- Add Stacks.js references to "Build app" nav
- Move BNS and Gaia to new protocols section in same nav
- Remove Stacks Connect page, merging contents into authentication guide
feat/build-apps
Mark Hendrickson 4 years ago
parent
commit
2574e3d934
  1. BIN
      public/images/app-sign-in.png
  2. 31
      src/common/navigation.yaml
  3. 201
      src/pages/build-apps/guides/authentication.md
  4. 6
      src/pages/build-apps/guides/data-storage.md
  5. 285
      src/pages/build-apps/references/stacks-connect.md
  6. 7
      src/pages/build-apps/tutorials/todos.md

BIN
public/images/app-sign-in.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

31
src/common/navigation.yaml

@ -59,19 +59,36 @@ sections:
usePageTitles: true
pages:
- path: /tutorials/todos
- path: /tutorials/angular
- path: /tutorials/indexing
- path: /tutorials/public-registry
- path: /tutorials/angular
- title: Stacks.js References
usePageTitles: true
pages:
- external:
href: 'https://github.com/blockstack/stacks.js/tree/master/packages/auth#stacksauth'
title: auth
- external:
href: 'https://github.com/blockstack/ux/tree/master/packages/connect#stacksconnect'
title: connect
- external:
href: 'https://github.com/blockstack/stacks.js/tree/master/packages/storage#stacksstorage'
title: storage
- external:
href: 'https://github.com/blockstack/stacks.js/tree/master/packages/network#stacksnetwork'
title: network
- external:
href: 'https://github.com/blockstack/stacks.js/tree/master/packages/stacking#stacksstacking'
title: stacking
- external:
href: 'https://github.com/blockstack/stacks.js/tree/master/packages/transactions#stackstransactions'
title: transactions
- title: References
- title: Protocols
usePageTitles: true
pages:
- path: /references/stacks-connect
- path: /references/bns
- path: /references/gaia
- external:
href: 'https://blockstack.github.io/stacks.js/'
title: Stacks.js
- path: /start-mining
- path: /references

201
src/pages/build-apps/guides/authentication.md

@ -11,7 +11,7 @@ images:
## Introduction
This guide explains how to authenticate users with [the Stacks Connect protocol](/build-apps/references/stacks-connect) by implementing the `connect` package of [Stacks.js](https://blockstack.github.io/stacks.js/).
This guide explains how to authenticate users with the [`connect`](https://github.com/blockstack/ux/tree/master/packages/connect#stacksconnect) package of Stacks.js.
Authentication provides a way for users to identify themselves to an app while retaining complete control over their credentials and personal details. It can be integrated alone or used in conjunction with [transaction signing](/build-apps/tutorials/transaction-signing) and [data storage](/build-apps/tutorials/data-storage), for which it is a prerequisite.
@ -19,8 +19,64 @@ Users who register for your app can subsequently authenticate to any other app w
See [the Todos app tutorial](/build-apps/tutorials/todos) for a concrete example of this functionality in practice.
## How it works
The authentication flow with Stacks is similar to the typical client-server flow used by centralized sign in services (for example, OAuth). However, with Stacks the authentication flow happens entirely client-side.
An app and authenticator, such as [the Stacks Wallet](https://blockstack.org/wallet), communicate during the authentication flow by passing back and forth two tokens. The requesting app sends the authenticator an `authRequest` token. Once a user approves authentication, the authenticator responds to the app with an `authResponse` token.
These tokens are are based on [a JSON Web Token (JWT) standard](https://tools.ietf.org/html/rfc7519) with additional support for the `secp256k1` curve used by Bitcoin and many other cryptocurrencies. They are passed via URL query strings.
See the [`authRequest`](#authrequest-payload-schema) and [`authResponse`](#authresponse-payload-schema) payload schemas below for more details about what data they contain.
When a user chooses to authenticate an app, it sends the `authRequest` token to the authenticator via a URL query string with an equally named parameter:
`https://wallet.hiro.so/...?authRequest=j902120cn829n1jnvoa...`
When the authenticator receives the request, it generates an `authResponse` token for the app using an _ephemeral transit key_ . The ephemeral transit key is just used for the particular instance of the app, in this case, to sign the `authRequest`.
The app stores the ephemeral transit key during request generation. The public portion of the transit key is passed in the `authRequest` token. The authenticator uses the public portion of the key to encrypt an _app private key_ which is returned via the `authResponse`.
The authenticator generates the app private key from the user's _identity address private key_ and the app's domain. The app private key serves three functions:
1. It is used to create credentials that give the app access to a storage bucket in the user's Gaia hub
2. It is used in the end-to-end encryption of files stored for the app in the user's Gaia storage.
3. It serves as a cryptographic secret that apps can use to perform other cryptographic functions.
Finally, the app private key is deterministic, meaning that the same private key will always be generated for a given Stacks address and domain.
The first two of these functions are particularly relevant to [data storage with Stacks.js](/build-apps/guides/data-storage).
[Learn more about keypairs](#key-pairs) used by authentication.
## Initiate userSession object
Apps keep track of user authentication state with the `userSession` object, initiated with the `UserSession` and `AppConfig` classes:
```js
import { AppConfig, UserSession } from '@stacks/connect';
const appConfig = new AppConfig(['store_write', 'publish_data']);
const userSession = new UserSession({ appConfig });
```
The main thing to decide here is what permission scopes your app needs from the user during authentication.
Apps may request any of the following scopes:
| Scope | Definition |
| -------------- | ------------------------------------------------------------------------------- |
| `store_write` | Read and write data to the user's Gaia hub in an app-specific storage bucket. |
| `publish_data` | Publish data so other users of the app can discover and interact with the user. |
The default scopes are `['store_write']` if no `scopes` array is provided when initializing the `appConfig` object.
We recommend you initiate the `userSession` object just once in your app then reference it using imports where needed.
## Initiate authentication flow
Apps prompt both new and existing users to authenticate with the `showConnect` function:
```js
import { AppConfig, UserSession, showConnect } from '@stacks/connect';
@ -35,14 +91,29 @@ function authenticate() {
},
redirectTo: '/',
finished: () => {
window.location.reload();
let userData = userSession.loadUserData();
// Save or otherwise utilize userData post-authentication
},
userSession: userSession,
});
}
```
## Handle pending sign in (still needed??)
`showConnect` triggers the display of a modal that initiates the authentication process for users, one in which they'll authenticate with a _Secret Key_ that's used to encrypt their private data.
![Modal displayed by showConnect function](/images/todos/get-started.png)
The `showConnect` function accepts a number of properties within a parameter object such as:
- The app's `name` and `icon`: provided as strings comprising the `appDetails` object property.
- The `redirectTo` string: used to provide a URL to which the user should be redirected upon successful authentication. The `finished` callback serves a similar purpose by handling successful authentication within a context of a popup window.
- The `userSession` object initiated above.
Once the user selects the button presented in this modal, they are passed to the Stacks Wallet for authenticator with the `authRequest` token as a GET parameter. From there they can confirm authentication and generate a new _Secret Key_ or Stacks identity before doing so, as needed before coming back to the app.
## Handle pending authentication
Unless the user has confirmed authentication within the context of a popup window, they will get redirected back to the app via the `redirectTo` address provided above, at which point the app needs to handle the pending authentication state using the `authResponse` value provided as a GET parameter:
```jsx
import { AppConfig, UserSession, showConnect } from '@stacks/connect';
@ -50,14 +121,130 @@ import { AppConfig, UserSession, showConnect } from '@stacks/connect';
const appConfig = new AppConfig(['store_write', 'publish_data']);
const userSession = new UserSession({ appConfig });
function componentDidMount() {
window.onload = function () {
if (userSession.isSignInPending()) {
userSession.handlePendingSignIn().then(userData => {
window.history.replaceState({}, document.title, '/');
this.setState({ userData: userData });
// Save or otherwise utilize userData post-authentication
});
} else if (userSession.isUserSignedIn()) {
this.setState({ userData: userSession.loadUserData() });
// Handle case in which user is already authenticated
}
};
```
The `isSignInPending` method of the `userSession` object is used to detect whether the user needs to handle a pending authentication state upon page load.
The `handlePendingSignIn` method is then used to handle that state, returning a `userData` object with all the data needed to save the user's information into their session.
The authenticated state can later be detected by the `isUserSignedIn` method in case any particular handling is needed then.
~> Note that implementing `handlePendingSignIn` is especially important for supporting authentication within the context of mobile apps.
If the user has indeed confirmed authentication in the context of a popup window, the authenticator will resolve the pending authentication state automatically with the app within the parent window.
It will then trigger the `finished` function provided above, which can be used similarly to save the user's information into their session as retrieved with `userSession.loadUserData()`.
## Key pairs
Authentication with Stacks makes extensive use of public key cryptography generally and ECDSA with the `secp256k1` curve in particular.
The following sections describe the three public-private key pairs used, including how they're generated, where they're used and to whom private keys are disclosed.
### Transit private key
The transit private is an ephemeral key that is used to encrypt secrets that
need to be passed from the authenticator to the app during the
authentication process. It is randomly generated by the app at the beginning of
the authentication response.
The public key that corresponds to the transit private key is stored in a single
element array in the `public_keys` key of the authentication request token. The
authenticator encrypts secret data such as the app private key using this
public key and sends it back to the app when the user signs in to the app. The
transit private key signs the app authentication request.
### Identity address private key
The identity address private key is derived from the user's keychain phrase and
is the private key of the Stacks username that the user chooses to use to sign in
to the app. It is a secret owned by the user and never leaves the user's
instance of the authenticator.
This private key signs the authentication response token for an app to indicate that the user approves sign in to that app.
### App private key
The app private key is an app-specific private key that is generated from the
user's identity address private key using the `domain_name` as input. It is
deterministic in that for a given Stacks username and `domain_name`, the same
private key is generated each time.
The app private key is securely shared with the app on each authentication, encrypted by the authenticator with the transit public key.
## authRequest Payload Schema
```jsx
const requestPayload = {
jti, // UUID
iat, // JWT creation time in seconds
exp, // JWT expiration time in seconds
iss, // legacy decentralized identifier generated from transit key
public_keys, // single entry array with public key of transit key
domain_name, // app origin
manifest_uri, // url to manifest file - must be hosted on app origin
redirect_uri, // url to which the authenticator redirects user on auth approval - must be hosted on app origin
version, // version tuple
do_not_include_profile, // a boolean flag asking authenticator to send profile url instead of profile object
supports_hub_url, // a boolean flag indicating gaia hub support
scopes, // an array of string values indicating scopes requested by the app
};
```
## authResponse Payload Schema
```jsx
const responsePayload = {
jti, // UUID
iat, // JWT creation time in seconds
exp, // JWT expiration time in seconds
iss, // legacy decentralized identifier (string prefix + identity address) - this uniquely identifies the user
private_key, // encrypted private key payload
public_keys, // single entry array with public key
profile, // profile object or null if passed by profile_url
username, // Stacks username (if any)
core_token, // encrypted core token payload
email, // email if email scope is requested & email available
profile_url, // url to signed profile token
hubUrl, // url pointing to user's gaia hub
version, // version tuple
};
```
## Decode authRequest or authResponse
To decode a token and see what data it holds:
1. Copy the `authRequest` or `authResponse` string from the URL during authentication.
2. Navigate to [jwt.io](https://jwt.io/).
3. Paste the full token there.
The output should look similar to below:
```json
{
"jti": "f65f02db-9f42-4523-bfa9-8034d8edf459",
"iat": 1555641911,
"exp": 1555645511,
"iss": "did:btc-addr:1ANL7TNdT7TTcjVnrvauP7Mq3tjcb8TsUX",
"public_keys": ["02f08d5541bf611ded745cc15db08f4447bfa55a55a2dd555648a1de9759aea5f9"],
"domain_name": "http://localhost:8080",
"manifest_uri": "http://localhost:8080/manifest.json",
"redirect_uri": "http://localhost:8080",
"version": "1.3.1",
"do_not_include_profile": true,
"supports_hub_url": true,
"scopes": ["store_write", "publish_data"]
}
```
The `iss` property is a decentralized identifier or `did`. This identifies the user and the username to the app. The specific `did` is a `btc-addr`.

6
src/pages/build-apps/guides/data-storage.md

@ -11,7 +11,7 @@ images:
## Introduction
This guide explains how to save and retrieve data for users with [Gaia](/build-apps/references/gaia) by implementing the `connect` and `storage` packages of [Stacks.js](https://blockstack.github.io/stacks.js/).
This guide explains how to save and retrieve data for users with [Gaia](/build-apps/references/gaia) by implementing the [`connect`](https://github.com/blockstack/ux/tree/master/packages/connect#stacksconnect) and [`storage`](https://github.com/blockstack/ux/tree/master/packages/storage#stacksstorage) packages of Stacks.js.
Data storage provides a way for users to save both public and private data off-chain while retaining complete control over it.
@ -19,6 +19,10 @@ Storing data off of the blockchain ensures that apps can provide users with high
See [the Todos app tutorial](/build-apps/tutorials/todos) for a concrete example of this functionality in practice.
## Authentication
TODO: Add indicator that authentication guide should be followed first
## How data is stored
Gaia storage is a key-value store.

285
src/pages/build-apps/references/stacks-connect.md

@ -1,285 +0,0 @@
---
title: Stacks Connect
description: Open protocol for connecting apps built with Stacks
images:
large: /images/pages/authentication.svg
sm: /images/pages/authentication-sm.svg
---
Stacks Connect is an open protocol for connecting apps built with the Stacks blockchain, such as consumer apps with authenticators and wallets.
## Authentication flow
For an application developer, the application flow is similar to the typical client-server flow used by centralized sign in services (for example, OAuth). However, with Stacks Connect, the authentication flow happens entirely client-side.
An app and authenticator, such as [the Stacks Wallet](https://blockstack.org/wallet), communicate during the authentication flow by passing back and forth two tokens. The requesting application sends the authenticator an `authRequest` token. Once a user approves a sign-in, the authenticator responds to the application with an `authResponse` token. These tokens are <a href="https://jwt.io/" target="\_blank">JSON Web Tokens</a>, and they are passed via URL query strings.
![](/images/app-sign-in.png)
When a user chooses to authenticate a decentralized application, it calls the `doOpenAuth()` method which sends an `authRequest` to the authenticator. Stacks auth passes the token in via a URL query string in the `authRequest` parameter:
`https://app.blockstack.org/#/sign-up?authRequest=j902120cn829n1jnvoa...`
When the authenticator receives the request, it generates an (`authResponse`) token to the application using an _ephemeral transit key_ . The ephemeral transit key is just used for the particular instance of the application, in this case, to sign the `authRequest`. The application stores the ephemeral transit key during the request generation. The public portion of the transit key is passed in the `authRequest` token. The authenticator uses the public portion of the key to encrypt an _app-private key_ which is returned via the `authResponse`.
During sign in, the authenticator generates the app-private key from the user's _identity-address private_ key and the application's `appDomain`. The app private key serves three functions:
- It is used to create the credentials that give an app access to the Gaia storage bucket for that specific app.
- It is used in the end-to-end encryption of files stored for the app in the user's Gaia storage.
- It serves as a cryptographic secret that apps can use to perform other cryptographic functions.
Finally, the app private key is deterministic, meaning that for a given user ID and domain name, the same private key is generated each time.
## Scopes
Scopes define the permissions requested by an app for granting during authentication.
Apps may request any of the following scopes:
| Scope | Definition |
| -------------- | ------------------------------------------------------------------------------------ |
| `store_write` | Read and write data to the user's Gaia hub in an app-specific storage bucket. |
| `publish_data` | Publish data so that other users of the app can discover and interact with the user. |
The permissions scope should be specified through the [`AppConfig`](https://blockstack.github.io/stacks.js/classes/appconfig.html)
object. If no `scopes` array is provided to the `redirectToSignIn` or `makeAuthRequest` functions, the default is to request `['store_write']`.
## Manifest file
Decentralized apps have a manifest file. This file is based on the [W3C web app manifest specification](https://w3c.github.io/manifest/).
The following is an example manifest file.
```json
{
"name": "Todo App",
"start_url": "http://todos.blockstack.org",
"description": "A simple todo app build on Stacks",
"icons": [
{
"src": "http://todos.blockstack.org/logo.png",
"sizes": "400x400",
"type": "image/png"
}
]
}
```
The Stacks Wallet retrieves the manifest file from the app during the authentication process and displays the
information in it such as the app `name` and to the user during sign in. The location of the app manifest file is specific
in the authentication request token and **must** be on the same origin as the app requesting authentication.
The manifest file **must** have [Cross-origin resource sharing (CORS) headers](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing)
that allow the manifest file to be fetched from any arbitrary source. This usually means returning a header like this:
```
Access-Control-Allow-Origin: *
```
How you implement CORS depends in part on which platform/service you use to serve your application. For example, Netlify
and Firebase have two different ways of configuring CORS. Consult your vendor documentation for more information.
## Key pairs
Stacks Auth makes extensive use of public key cryptography. Blockstack uses ECDSA with the `secp256k1` curve. The
following sections describe the three public-private key pairs used in the authentication process:
- how they're generated
- where they're used
- to whom the private key is disclosed
### Transit private key
The transit private is an ephemeral key that is used to encrypt secrets that
need to be passed from the authenticator to the decentralized app during the
authentication process. It is randomly generated by the app at the beginning of
the authentication response.
The public key that corresponds to the transit private key is stored in a single
element array in the `public_keys` key of the authentication request token. The
authenticator encrypts secret data such as the app private key using this
public key and sends it back to the app when the user signs in to the app. The
transit private key signs the app authentication request.
### Identity address private key
The identity address private key is derived from the user's keychain phrase and
is the private key of the Stacks username that the user chooses to use to sign in
to the app. It is a secret owned by the user and never leaves the user's
instance of the authenticator.
This private key signs the authentication response token for an app to indicate that the user approves sign in to that app.
### App private key
The app private key is an app-specific private key that is generated from the
user's identity address private key using the `domain_name` as input. It is
deterministic in that for a given Stacks username and `domain_name`, the same
private key is generated each time.
The app private key is securely shared with the app on each authentication, encrypted by the authenticator with the transit public key.
## JSON Web Token signatures
Both the `authRequest` and the `authResponse` tokens are [JSON Web Tokens](https://jwt.io/), and they are passed via URL query strings.
Stacks authentication tokens are based on the [RFC 7519 OAuth JSON Web Token (JWT)](https://tools.ietf.org/html/rfc7519)
with additional support for the `secp256k1` curve used by Bitcoin and many other
cryptocurrencies.
This signature algorithm is indicated by specifying `ES256K` in the token's
`alg` key, specifying that the JWT signature uses ECDSA with the secp256k1
curve. Stacks auth provide both [JavaScript](https://github.com/blockstack/jsontokens-js)
and
[Ruby](https://github.com/blockstack/ruby-jwt-blockstack/tree/ruby-jwt-blockstack)
JWT libraries with support for this signing algorithm.
-> The Stacks JWT implementation is different from other implementations because of the underlying cryptography we employ. There are libraries in [JavaScript](https://github.com/blockstack/jsontokens-js) and [Ruby](https://github.com/blockstack/ruby-jwt-blockstack) available on the Blockstack Github to allow you to work with these tokens.
### Example: authRequest payload schema
```jsx
const requestPayload = {
jti, // UUID
iat, // JWT creation time in seconds
exp, // JWT expiration time in seconds
iss, // legacy decentralized identifier generated from transit key
public_keys, // single entry array with public key of transit key
domain_name, // app origin
manifest_uri, // url to manifest file - must be hosted on app origin
redirect_uri, // url to which the authenticator redirects user on auth approval - must be hosted on app origin
version, // version tuple
do_not_include_profile, // a boolean flag asking authenticator to send profile url instead of profile object
supports_hub_url, // a boolean flag indicating gaia hub support
scopes, // an array of string values indicating scopes requested by the app
};
```
### Example: authResponse payload schema
```jsx
const responsePayload = {
jti, // UUID
iat, // JWT creation time in seconds
exp, // JWT expiration time in seconds
iss, // legacy decentralized identifier (string prefix + identity address) - this uniquely identifies the user
private_key, // encrypted private key payload
public_keys, // single entry array with public key
profile, // profile object or null if passed by profile_url
username, // Stacks username (if any)
core_token, // encrypted core token payload
email, // email if email scope is requested & email available
profile_url, // url to signed profile token
hubUrl, // url pointing to user's gaia hub
version, // version tuple
};
```
## Decode authRequest
To decode the token and see what information it holds:
1. Copy the `authRequest` string from the URL.
<img src="/images/copy-authRequest.png" alt="" />
2. Navigate to [jwt.io](https://jwt.io/).
3. Paste the full token there.
The output should look similar to below:
```json
{
"jti": "f65f02db-9f42-4523-bfa9-8034d8edf459",
"iat": 1555641911,
"exp": 1555645511,
"iss": "did:btc-addr:1ANL7TNdT7TTcjVnrvauP7Mq3tjcb8TsUX",
"public_keys": ["02f08d5541bf611ded745cc15db08f4447bfa55a55a2dd555648a1de9759aea5f9"],
"domain_name": "http://localhost:8080",
"manifest_uri": "http://localhost:8080/manifest.json",
"redirect_uri": "http://localhost:8080",
"version": "1.3.1",
"do_not_include_profile": true,
"supports_hub_url": true,
"scopes": ["store_write", "publish_data"]
}
```
The `iss` property is a decentralized identifier or `did`. This identifies the user and the user name to the application. The specific `did` is a `btc-addr`.
## User profiles
Profile data is stored using Gaia on the user's selected storage provider. An example of a `profile.json` file URL using
default provided storage:
```
https://gaia.blockstack.org/hub/1EeZtGNdFrVB2AgLFsZbyBCF7UTZcEWhHk/profile.json
```
Follow these steps to create and register a profile for a BNS username (`identifier`):
1. Create a JSON profile object
2. Split up the profile into tokens, sign the tokens, and put them in a token file
3. Create a zone file that points to the web location of the profile token file
```jsx
"account": [
{
"@type": "Account",
"service": "twitter",
"identifier": "naval",
"proofType": "http",
"proofUrl": "https://twitter.com/naval/status/12345678901234567890"
}
]
```
## Create a profile
```jsx
const profileOfNaval = {
'@context': 'http://schema.org/',
'@type': 'Person',
name: 'Naval Ravikant',
description: 'Co-founder of AngelList',
};
```
## Sign a profile as a single token
```jsx
import { wrapProfileToken, Person } from '@stacks/profiles';
const privateKey = 'e546ba96ee34220287d0c177418011addf8d71b32fb81ae8e33a1d7510fa5d0d01';
const person = new Person(profileOfNaval);
const token = person.toToken(privateKey);
const tokenFile = [wrapProfileToken(token)];
```
## Verify an individual token
```jsx
import { verifyProfileToken } from '@stacks/profiles';
try {
const decodedToken = verifyProfileToken(tokenFile[0].token, publicKey);
} catch (e) {
console.log(e);
}
```
## Recover a profile from a token file
```jsx
const recoveredProfile = Person.fromToken(tokenFile, publicKey);
```
## Validate profile schema
```jsx
const validationResults = Person.validateSchema(recoveredProfile);
```
### Transaction signing
TBD: info on transaction signing protocol

7
src/pages/build-apps/tutorials/todos.md

@ -69,10 +69,9 @@ You should see the app's landing page:
### Step 1: Choose **Get started** to start onboarding into the app.
The app displays a standardized introductory modal using
Stacks Connect.
The app displays a standardized introductory modal using the `@stacks/connect` library.
![The Stacks Connect Modal](/images/todos/get-started.png)
![Modal displayed by showConnect function](/images/todos/get-started.png)
This modal is displayed using the `authenticate` function exported by the `src/auth.js` module, which organizes all Stacks resources needed for authentication in the app:
@ -109,7 +108,7 @@ export function getPerson() {
}
```
The `authenticate` function implements the `showConnect` function imported from the `@stacks/connect` library.
The `authenticate` function implements the `showConnect` function imported from the `connect` package of Stacks.js.
`showConnect` triggers the display of a modal that initiates the authentication process for users, one in which they'll authenticate with a _Secret Key_ that's used to encrypt their private data.

Loading…
Cancel
Save