18.1.5
Blockstack Authentication provides single sign on and authentication without third parties or remote servers. Blockstack Authentication is a bearer token-based authentication system. From an app user's perspective, it functions similar to legacy third-party authentication techniques that they're familiar with. For an app developer, the flow is a bit different from the typical client-server flow of centralized sign in services (e.g., OAuth). Rather, with Blockstack, the authentication flow happens entirely client-side.
1) Install blockstack.js
:
npm install blockstack --save
2) Import Blockstack into your project
import * as blockstack from 'blockstack'
3) Wire up a sign in button
document.getElementById('signin-button').addEventListener('click', function() {
blockstack.redirectToSignIn()
})
4) Wire up a sign out button
document.getElementById('signout-button').addEventListener('click', function() {
blockstack.signUserOut(window.location.origin)
})
5) Include the logic to (a) load user data (b) handle the auth response
function showProfile(profile) {
var person = new blockstack.Person(profile)
document.getElementById('heading-name').innerHTML = person.name()
document.getElementById('avatar-image').setAttribute('src', person.avatarUrl())
document.getElementById('section-1').style.display = 'none'
document.getElementById('section-2').style.display = 'block'
}
if (blockstack.isUserSignedIn()) {
const userData = blockstack.loadUserData()
showProfile(userData.profile)
} else if (blockstack.isSignInPending()) {
blockstack.handlePendingSignIn()
.then(userData => {
showProfile(userData.profile)
})
}
6) Create a manifest.json
file
{
"name": "Hello, Blockstack",
"start_url": "localhost:5000",
"description": "A simple demo of Blockstack Auth",
"icons": [{
"src": "https://helloblockstack.com/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
}]
}
Make sure your manifest.json
file has appropriate CORS headers so that it can
be fetched via an http GET
from any origin.
7) Serve your application
What follows is a walk through of the experience of a user, Alice, signing in to your app with Blockstack.
First, Alice clicks the "Sign in with Blockstack" button on your app. She is redirected to her copy of the Blockstack Browser. The Blockstack Browser shows Alice an approval dialog with information about your app including:
Alice can choose to authenticate as one of her Blockstack IDs by selecting the ID and clicking the Approve button.
When she clicks approve, she's redirected back to your app. Your app gets cryptographic proof that she is who she claims to be, access to a dedicated bucket in her Gaia storage hub for your app to read and write its own data along with public information she's stored in her profile.
Blockstack apps have a manifest file based on the W3C web app manifest specification. The Blockstack Browser retrieves the manifest file from the app during the authentication process and displays some of the information in it such as the app name and icon to the user. 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.
Below is an example of a manifest file:
{
"name": "Todo App",
"start_url": "http://blockstack-todos.appartisan.com",
"description": "A simple todo app build on blockstack",
"icons": [{
"src": "http://blockstack-todos.appartisan.com/logo.png",
"sizes": "400x400",
"type": "image/png"
}]
}
The manifest file MUST have Cross-origin resource sharing (CORS) headers that allow the manifest file to be fetched from any arbitrary source. This usually means returning:
Access-Control-Allow-Origin: *
Blockstack Authentication makes extensive use of public key cryptography. As mentioned above, we use ECDSA with the secp256k1 curve. What follows is a description of the various public-private key pairs used in the authentication process including how they're generated, where they're used and to whom the private key is disclosed.
The transit private is an ephemeral key that is used to encrypt secrets that need to be passed from the Blockstack Browser 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 Blockstack Browser 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.
The identity address private key is derived from the user's keychain phrase and is the private key of the Blockstack ID 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 Blockstack browser. This private key signs the authentication response token for an app to indicate that the user approves sign in to that app.
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 Blockstack ID and domain_name
, the same private key will be generated each time. The app private key is securely shared with the app on each authentication, encrypted by the Blockstack browser with the transit public key.
The app private key serves three functions.
Scopes define the information and permissions an app requests from the user during authentication. Requested scopes may be any of the following:
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
email
- requests the user's email if available
If no scopes
array is provided to the redirectToSignIn
or
makeAuthRequest
functions, the default is to request ['store_write']
.
The app and the Blockstack Browser communicate during the authentication flow by passing back and forth two tokens:
The requesting application sends the Blockstack Browser an authRequest token. Once a user approves a sign in, the Blockstack Browser responds to the application with an authResponse token.
These tokens are JSON Web Tokens, and they are passed via URL query strings.
Blockstack's authentication tokens are based on the RFC 7519 OAuth JSON Web Token (JWT) 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. We provide both JavaScript and Ruby JWT libraries with support for this signing algorithm.
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 browser redirects user on auth approval - must be hosted on app origin
version, // version tuple
do_not_include_profile, // a boolean flag asking browser 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
}
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, // blockstack id 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
}
blockstack:
custom protocol handlerThe blockstack:
custom protocol handler is how Blockstack apps send their authentication requests to the Blockstack Browser. When the Blockstack Browser is installed on a user's computer, it registers itself as the handler for the blockstack:
customer protocol.
When an application calls redirectToSignIn
or redirectToSignInWithAuthRequest
, blockstack.js checks if a blockstack: protocol handler is installed and, if so, redirects the user to blockstack:<authRequestToken>
. This passes the authentication request token from the app to the Blockstack Browser, which will in turn validate the request and display an authentication dialog.
The way you can add Blockstack Authentication to you app depends on whether your app is a modern decentralized Blockstack App where code runs client-side without trusted servers or a legacy client-server app where a server is trusted.
This method is appropriate for decentralized client-side apps where the user's zone of trust - the parts of the app that the user is trusting - begins and ends with the code running on their own computer. In apps like these, any code the app interacts with that's not on their own computer such as external servers does not need to know who she is.
Blockstack.js provides API methods that help you to implement Blockstack Authentication in your client-side app.
The preferred way to implement authentication in these apps is to use the standard flow. This flow hides much of the process behind a few easy function calls and makes it very fast to get up and running.
In this process you'll use these four functions:
redirectToSignIn
isSignInPending
handlePendingSignIn
loadUserData
When your app wants to start the sign in process, typically when the user clicks a "Sign in with Blockstack" button, your app will call the redirectToSignIn
method of blockstack.js.
This creates an ephemeral transit key, stores it in the web browser's localStorage
, uses it to create an authentication request token and finally redirects the user to the Blockstack browser to approve the sign in request.
When a user approves a sign in request, the Blockstack Browser will return the signed authentication response token to the redirectURI
specified in redirectToSignIn
.
To check for the presence of this token, your app should call isSignInPending
. If this returns true
, the app should then call handlePendingSignIn
. This decodes the token, returns the signed-in-user's data, and simultaneously storing it to localStorage
so that it can be retrieved later with loadUserData
.
import * as blockstack from 'blockstack'
if (blockstack.isSignInPending()) {
blockstack.handlePendingSignIn()
.then(userData => {
const profile = userData.profile
})
}
Alternatively, you can manually generate your own transit private key and/or authentication request token. This gives you more control over the experience.
For example, you could use the following code to generate an authentication request on https://alice.example.com
or https://bob.example.com
for an app running on origin https://example.com
.
const transitPrivateKey = generateAndStoreTransitKey()
const redirectURI = 'https://example.com/authLandingPage'
const manifestURI = 'https://example.com/manifest.json'
const scopes = ['scope_write', 'publish_data']
const appDomain = 'https://example.com'
const authRequest = makeAuthRequest(transitPrivateKey, redirectURI, manifestURI, scopes, appDomain)
redirectToSignInWithAuthRequest(authRequest)
Note: Client-server authentication requires using a library written in the language of your server app. There are private methods in blockstack.js that can be accomplish this on node.js server apps, but they are not currently part of our public, supported API.
Using Blockstack Authentication in client-server apps is very similar to client-side apps. You generate the authentication request using the same code in the client as described above.
The main difference is that you need to verify the authentication response token on the server after the user approves sign in to your app.
For an example of how verification can be done server side, take a look at the blockstack-ruby library.
Generates an authentication request and redirects the user to the Blockstack browser to approve the sign in request.
Please note that this requires that the web browser properly handles the
blockstack:
URL protocol handler.
Most applications should use this
method for sign in unless they require more fine grained control over how the
authentication request is generated. If your app falls into this category,
use makeAuthRequest
and redirectToSignInWithAuthRequest
to build your own sign in process.
(String
= `${window.location.origin}/`
)
The location to which the identity provider will redirect the user after
the user approves sign in.
(String
= `${window.location.origin}/manifest.json`
)
Location of the manifest file.
(Array
= DEFAULT_SCOPE
)
Defaults to requesting write access to
this app's data store.
An array of strings indicating which permissions this app is requesting.
void
:
Check if there is a authentication request that hasn't been handled.
Boolean
:
true
if there is a pending sign in, otherwise
false
Try to process any pending sign in request by returning a Promise
that resolves
to the user data object if the sign in succeeds.
(String
= ''
)
the endpoint against which to verify public
keys match claimed username
(String
= getAuthResponseToken()
)
the signed authentication response token
(String
= getTransitKey()
)
the transit private key that corresponds to the transit public key
that was provided in the authentication request
Promise
:
that resolves to the user data object if successful and rejects
if handling the sign in request fails or there was no pending sign in request.
Retrieves the user data object. The user's profile is stored in the key profile
.
Object
:
User data object.
Check if a user is currently signed in.
Boolean
:
true
if the user is signed in,
false
if not.
Sign the user out and optionally redirect to given location.
(String
= null
)
Location to redirect user to after sign out.
void
:
Generates an authentication request that can be sent to the Blockstack
browser for the user to approve sign in. This authentication request can
then be used for sign in by passing it to the redirectToSignInWithAuthRequest
method.
Note: This method should only be used if you want to roll your own authentication
flow. Typically you'd use redirectToSignIn
which takes care of this
under the hood.
(String
= generateAndStoreTransitKey()
)
hex encoded transit
private key
(String
= `${window.location.origin}/`
)
location to redirect user to after sign in approval
(String
= `${window.location.origin}/manifest.json`
)
location of this app's manifest file
(String
= window.location.origin
)
the origin of this app
(Number
= nextHour().getTime()
)
the time at which this request is no longer valid
String
:
the authentication request
Generates a ECDSA keypair to use as the ephemeral app transit private key and stores the hex value of the private key in local storage.
String
:
the hex encoded private key
Redirects the user to the Blockstack browser to approve the sign in request given.
The user is redirected to the blockstackIDHost
if the blockstack:
protocol handler is not detected. Please note that the protocol handler detection
does not work on all browsers.
(String
= DEFAULT_BLOCKSTACK_HOST
)
the URL to redirect the user to if the blockstack
protocol handler is not detected
void
:
Retrieve the authentication token from the URL query
String
:
the authentication token if it exists otherwise
null
Follow these steps to create and register a profile for a Blockchain ID:
const profileOfNaval = { "@context": "http://schema.org/", "@type": "Person", "name": "Naval Ravikant", "description": "Co-founder of AngelList" }
import { makeECPrivateKey, wrapProfileToken, Person } from 'blockstack' const privateKey = makeECPrivateKey() const person = new Person(profileOfNaval) const token = person.toToken(privateKey) const tokenFile = [wrapProfileToken(token)]
import { verifyProfileToken } from 'blockstack'
try {
const decodedToken = verifyProfileToken(tokenFile[0].token, publicKey)
} catch(e) {
console.log(e)
}
const recoveredProfile = Person.fromToken(tokenFile, publicKey)
const validationResults = Person.validateSchema(recoveredProfile)
Profile data is stored using Gaia on the user's selected storage provider.
An example of a profile.json file URL using Blockstack provided storage:
https://gaia.blockstack.org/hub/1EeZtGNdFrVB2AgLFsZbyBCF7UTZcEWhHk/profile.json
import { validateProofs } from 'blockstack' const domainName = "naval.id" validateProofs(profile, domainName).then((proofs) => { console.log(proofs) })
The validateProofs
function checks each of the proofs listed in the
profile by fetching the proof URL and verifying the proof message.
The proof message must be of the form:
Verifying my Blockstack ID is secured with the address
1EeZtGNdFrVB2AgLFsZbyBCF7UTZcEWhHk
The proof message also must appear in the required location on the proof page specific to each type of social media account.
The account from which the proof message is posted must match exactly
the account identifier/username claimed in the user profile. The
validateProofs
function will check this in the body of the proof or
in the proof URL depending on the service.
The Service
class can be extended to provide proof validation service
to additional social account types. You will need to override the
getProofStatement(searchText: string)
method which parses the proof
body and returns the proof message text. Additionally, the identifier
claimed should be verified in the proof URL or in the body by implementing
getProofIdentity(searchText: string)
and setting shouldValidateIdentityInBody()
to return true.
The following snippet uses the meta tags in the proof page to retrieve the proof message.
static getProofStatement(searchText: string) {
const $ = cheerio.load(searchText)
const statement = $('meta[property="og:description"]')
.attr('content')
if (statement !== undefined && statement.split(':').length > 1) {
return statement.split(':')[1].trim().replace('“', '').replace('”', '')
} else {
return ''
}
}
Proofs are stored under the account
key in the user's profile data
"account": [
{
"@type": "Account",
"service": "twitter",
"identifier": "naval",
"proofType": "http",
"proofUrl": "https://twitter.com/naval/status/12345678901234567890"
}
]
Extracts a profile from an encoded token and optionally verifies it,
if publicKeyOrAddress
is provided.
(String)
the token to be extracted
(String
= null
)
the public key or address of the
keypair that is thought to have signed the token
Object
:
the profile extracted from the encoded token
publicKeyOrAddress
Wraps a token for a profile token file
(String)
the token to be wrapped
Object
:
including
token
and
decodedToken
Signs a profile token
(Object)
the JSON of the profile to be signed
(String)
the signing private key
(Object
= null
)
the entity that the information is about
(Object
= null
)
the entity that is issuing the token
(String
= 'ES256K'
)
the signing algorithm to use
(Date
= new Date()
)
the time of issuance of the token
(Date
= nextYear()
)
the time of expiration of the token
Object
:
the signed profile token
Verifies a profile token
(String)
the token to be verified
(String)
the public key or address of the
keypair that is thought to have signed the token
Object
:
the verified, decoded profile token
Validates the social proofs in a user's profile. Currently supports validation of Facebook, Twitter, GitHub, Instagram, LinkedIn and HackerNews accounts.
(Object)
The JSON of the profile to be validated
(string)
The owner bitcoin address to be validated
(string
= null
)
The Blockstack name to be validated
Promise
:
that resolves to an array of validated proof objects
Look up a user profile by blockstack ID
(string)
The Blockstack ID of the profile to look up
(string
= null
)
The URL
to use for zonefile lookup. If falsey, lookupProfile will use the
blockstack.js getNameInfo function.
Promise
:
that resolves to a profile object
Notes:
1) Blockstack Gaia Storage APIs and on-disk format will change in upcoming pre-releases breaking backward compatibility. File encryption is currently opt-in on a file by file basis.
2) Certain storage features such as and collections are not implemented in the current version. These features will be rolled out in future updates.
blockstack.putFile("/hello.txt", "hello world!")
.then(() => {
// /hello.txt exists now, and has the contents "hello world!".
})
blockstack.getFile("/hello.txt")
.then((fileContents) => {
// get the contents of the file /hello.txt
assert(fileContents === "hello world!")
});
let options = {
encrypt: true
}
blockstack.putFile("/message.txt", "Secret hello!", options)
.then(() => {
// message.txt exists now, and has the contents "hello world!".
})
let options = {
decrypt: true
}
blockstack.getFile("/message.txt", options)
.then((fileContents) => {
// get & decrypt the contents of the file /message.txt
assert(fileContents === "Secret hello!")
});
In order for files to be publicly readable, the app must request
the publish_data
scope during authentication.
let options = {
user: 'ryan.id', // the Blockstack ID of the user for which to lookup the file
app: 'http://BlockstackApp.com' // origin of the app this file is stored for
}
blockstack.getFile("/message.txt", options)
.then((fileContents) => {
// get the contents of the file /message.txt
assert(fileContents === "hello world!")
});
Note: deleteFile is currently not implemented. For now, we recommend writing an empty file to wipe data
blockstack.deleteFile("/hello.txt")
.then(() => {
// /hello.txt is now removed.
})
Retrieves the specified file from the app's data store.
(String)
the path to the file to read
(Object
= null
)
options object
Name | Description |
---|---|
options.decrypt Boolean
(default true )
|
try to decrypt the data with the app private key |
options.username String
|
the Blockstack ID to lookup for multi-player storage |
options.verify Boolean
|
Whether the content should be verified, only to be used
when
putFile
was set to
sign = true
|
options.app String
|
the app to lookup for multi-player storage - defaults to current origin |
options.zoneFileLookupURL String
(default null )
|
The URL to use for zonefile lookup. If falsey, this will use the blockstack.js's getNameInfo function instead. |
Promise
:
that resolves to the raw data in the file
or rejects with an error
Stores the data provided in the app's data store to to the file specified.
(String)
the path to store the data in
(Object
= null
)
options object
Name | Description |
---|---|
options.encrypt (Boolean | String)
(default true )
|
encrypt the data with the app private key or the provided public key |
options.sign Boolean
(default false )
|
sign the data using ECDSA on SHA256 hashes with the app private key |
options.contentType String
(default '' )
|
set a Content-Type header for unencrypted data |
Promise
:
that resolves if the operation succeed and rejects
if it failed
Encrypts the data provided with the app public key.
String
:
Stringified ciphertext object
Decrypts data encrypted with encryptContent
with the
transit private key.
(String | Buffer)
:
decrypted content.
Get the app storage bucket URL
(String)
the gaia hub URL
(String)
the app private key used to generate the app address
Promise
:
That resolves to the URL of the app index file
or rejects if it fails
Fetch the public read URL of a user file for the specified app.
(String)
the path to the file to read
(String)
The Blockstack ID of the user to look up
(String)
The app origin
(String
= null
)
The URL
to use for zonefile lookup. If falsey, this will use the
blockstack.js's getNameInfo function instead.
Promise
:
that resolves to the public read URL of the file
or rejects with an error
Get the price of a name.
(String)
the name to query
Promise
:
a promise to an Object with { units: String, amount: BigInteger }, where
.units encodes the cryptocurrency units to pay (e.g. BTC, STACKS), and
.amount encodes the number of units, in the smallest denominiated amount
(e.g. if .units is BTC, .amount will be satoshis; if .units is STACKS,
.amount will be microStacks)
Get the price of a namespace
(String)
the namespace to query
Promise
:
a promise to an Object with { units: String, amount: BigInteger }, where
.units encodes the cryptocurrency units to pay (e.g. BTC, STACKS), and
.amount encodes the number of units, in the smallest denominiated amount
(e.g. if .units is BTC, .amount will be satoshis; if .units is STACKS,
.amount will be microStacks)
How many blocks can pass between a name expiring and the name being able to be re-registered by a different owner?
Promise
:
a promise to the number of blocks
Get the names -- both on-chain and off-chain -- owned by an address.
(String)
the blockchain address (the hash of the owner public key)
Promise
:
a promise that resolves to a list of names (Strings)
Get the blockchain address to which a name's registration fee must be sent (the address will depend on the namespace in which it is registered.)
(String)
the namespace ID
Promise
:
a promise that resolves to an address (String)
Get WHOIS-like information for a name, including the address that owns it, the block at which it expires, and the zone file anchored to it (if available).
(String)
the name to query. Can be on-chain of off-chain.
Promise
:
a promise that resolves to the WHOIS-like information
Get the pricing parameters and creation history of a namespace.
(String)
the namespace to query
Promise
:
a promise that resolves to the namespace information.
Get a zone file, given its hash. Throws an exception if the zone file obtained does not match the hash.
(String)
the ripemd160(sha256) hash of the zone file
Promise
:
a promise that resolves to the zone file's text
Get the status of an account for a particular token holding. This includes its total number of expenditures and credits, lockup times, last txid, and so on.
Promise
:
a promise that resolves to an object representing the state of the account
for this token
Get a page of an account's transaction history.
(String)
the account's address
(number)
the page number. Page 0 is the most recent transactions
Promise
:
a promise that resolves to an Array of Objects, where each Object encodes
states of the account at various block heights (e.g. prior balances, txids, etc)
Get the state(s) of an account at a particular block height. This includes the state of the account beginning with this block's transactions, as well as all of the states the account passed through when this block was processed (if any).
(String)
the account's address
(Integer)
the block to query
Promise
:
a promise that resolves to an Array of Objects, where each Object encodes
states of the account at this block.
Get the set of token types that this account owns
(String)
the account's address
Promise
:
a promise that resolves to an Array of Strings, where each item encodes the
type of token this account holds (excluding the underlying blockchain's tokens)
Get the number of tokens owned by an account. If the account does not exist or has no tokens of this type, then 0 will be returned.
Promise
:
a promise that resolves to a BigInteger that encodes the number of tokens
held by this account.
Estimates the cost of a token-transfer transaction
(String)
the recipient of the tokens
(String)
the type of token to spend
(Object)
a 64-bit unsigned BigInteger encoding the number of tokens
to spend
(String)
an arbitrary string to store with the transaction
(Number
= 1
)
the number of utxos we expect will
be required from the importer address
(Number
= 1
)
the number of outputs we expect to add beyond
just the recipient output (default = 1, if the token owner is also the bitcoin funder)
Promise
:
a promise which resolves to the satoshi cost to
fund this token-transfer transaction
List the set of files in this application's Gaia storage bucket.
(function)
a callback to invoke on each named file that
returns
true
to continue the listing operation or
false
to end it
Promise
:
that resolves to the number of files listed