BlockStack身份授权流程

为什么需要应用授权

去中心化身份的前提条件,是在同一个身份平台所能覆盖的范围内,用户的身份识别和检测标准统一,作为区块链应用开发基础设施的服务提供商,BlockStack 在数据权限上将应用权限和用户身份/数据分离,保障用户数据所有权。

这种设计虽然实现起来较为复杂,且需要多种类型的服务提供支持,但不论是对用户,开发者,还是整个 Blockstack 生态,都是非常优雅的方案。

mmexport1585486832221.jpg

  • 用户
    • gaia 通过 app 域名隔离数据权限,无需担心全量数据安全
    • 可以使用多身份来管理相同的应用数据
    • 使用应用之前明确的清楚应用的权限范围
    • 可以将数据在不同应用之间迁移
  • 开发者
    • 无需单独实现账户注册与用户管理等服务
    • 不需要处理复杂的加密解密等校验逻辑
  • Blockstack
    • 一套 DID 身份与用户数据管理标准
    • 提供更多的应用基础设施服务

应用授权的流程

如下所示:

  • 构建 Token 并跳转 通过 BlockStack.js 所提供的 redirectToSignIn 方法跳转到 BlockStackBrowser 完成授权
    • 构建请求体 authRequest
      • generateAndStoreTransitKey 生成一个临时并随机的公私钥 Key
      • 返回一个经过编码的 authRequest 字符串
    • launchCustomProtocol 封装一系列的逻辑并跳转至 BlockStackBrowser
      • 添加一些超时和请求序号等操作
  • Browser 接收参数并解析 BlockStack 浏览器端接收到 authRequest 参数触发验证流程
    • app/js/auth/index.js 中使用 verifyAuthRequestAndLoadManifest 校验 authRequest 合法性并获取 DApp 的 Manifest.json 文件
      • verifyAuthRequest 校验 Token 的合法性
      • fetchAppManifest 获取应用 Manifest.json 文件
    • getFreshIdentities 通过用户缓存在浏览器中的登录信息 addresses(地址)获取用户的信息
      • 请求 https://core.blockstack.org/addresses/bitcoin/${address} 获得用户比特币地址的信息
      • 加载用户域名信息
      • 从 Gaia 获取用户 profile 文件的位置,并拿到用户的 profile 文件
    • 用户根据 profile 中包含的身份信息让用户选择需要授权的用户名,触发login
      • 客户端 noCoreStorage 作为监听标志来构造 authRespon
      • 获取用户的 profileUrl
      • 获取 app 的 bucketUrl
      • 创建并更新用户的 Profile 中 apps 的数据
      • 构建 AuthResponse Token
        • 生成 appPrivateKey
        • 生成 gaiaAssociationToken
      • 通过 BlockStack.js 的 redirectUserToApp 返回应用
      • redirect URI
  • APP 接受并解析 通过 AuthRrsponse 参数解析获取用户信息并持久化
    • 调用 userSession.SignInPendinguserSession.handlePendingSignIn 能够触发对 AuthResponse 的解析
    • 通过verifyAuthResponse 进行一系列的验证,fetchPrivate 获得授权用户的 profile 数据
    • 持久化用户数据到浏览器 localstorage
代码语言:javascript
复制
sequenceDiagram
    participant A as App
    participant B as Broswer
    participant G as Gaia
    participant C as BlockStack Core
    participant Bi as Bitcoin network
Note over A: Make authRequest
A->>+B: authRequest Token
Note over B: verifyAuthRequest
B->>-A: fetch Manifest.json
A->>B: Manifest.json
opt getFreshIdentities
    B->>Bi: nameLookup names
    Bi->>B: get names
    alt is has no name
        B->>+G: fetchProfileLocations
        G->>-B: profile data
    else is well
        B->>+C: nameLookupUrl
        C->>-B: nameInfo with zoneFile
    end
end 
note over B: render account list
opt user click login
     B->>+C: nameLookupUrl
     C->>-B: profile data
    note over B: verify APP Scope
    alt is name has no zonefile
        B->>G: fetchProfileLocations
        G->>B: profile url
    else is has zoneFile
        note over  B: Parse zonefile url
    end
    B->>G: getAppBucketUrl
    G->>B: appBucketUrl
    note over B: signProfileForUpload
    B-->> G: uploadProfile
    note over B: AuthResponse
    B->>A: AuthResponse Token
end
note over A: getAuthRes Token
A->>G: get profile
G->>A: profile data
note over A: store userData</code></pre></div></div><figure class=""><hr/></figure><h3 id="1vhae" name="%E4%BB%A3%E7%A0%81%E8%A7%A3%E6%9E%90">代码解析</h3><h4 id="erljd" name="%E6%9E%84%E5%BB%BA%E6%8E%88%E6%9D%83%E8%AF%B7%E6%B1%82">构建授权请求</h4><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言:</span>javascript</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><i class="icon-copy"></i><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre class="prism-token token line-numbers language-javascript"><code class="language-javascript" style="margin-left:0">// 触发授权请求

redirectToSignIn(
redirectURI?: string,
manifestURI?: string,
scopes?: Array<AuthScope | string>
): void {
const transitKey = this.generateAndStoreTransitKey() // 生成一个临时秘钥对
const authRequest = this.makeAuthRequest(transitKey, redirectURI, manifestURI, scopes) // 构建 AuthRequest
const authenticatorURL = this.appConfig && this.appConfig.authenticatorURL // 获取授权跳转链接
return authApp.redirectToSignInWithAuthRequest(authRequest, authenticatorURL) // 跳转
}

// 构建 AuthRequest 数据
makeAuthRequest(
transitKey?: string,
redirectURI?: string,
manifestURI?: string,
scopes?: Array<AuthScope | string>,
): string {
const appConfig = this.appConfig
transitKey = transitKey || this.generateAndStoreTransitKey()
redirectURI = redirectURI || appConfig.redirectURI()
manifestURI = manifestURI || appConfig.manifestURI()
scopes = scopes || appConfig.scopes
return authMessages.makeAuthRequest(
transitKey, redirectURI, manifestURI,
scopes)
}

// 构建请求授权 Token 详情
export function makeAuthRequest(
transitPrivateKey?: string,
redirectURI?: string,
manifestURI?: string,
scopes: Array<AuthScope | string> = DEFAULT_SCOPE.slice(),
appDomain?: string,
expiresAt: number = nextMonth().getTime(),
extraParams: any = {}
): string {
// ...
const payload = Object.assign({}, extraParams, {
jti: makeUUID4(),
iat: Math.floor(new Date().getTime() / 1000), // JWT times are in seconds
exp: Math.floor(expiresAt / 1000), // JWT times are in seconds
iss: null,
public_keys: [],
domain_name: appDomain,
manifest_uri: manifestURI,
redirect_uri: redirectURI,
version: VERSION,
do_not_include_profile: true,
supports_hub_url: true,
scopes
})
/* Convert the private key to a public key to an issuer */
const publicKey = SECP256K1Client.derivePublicKey(transitPrivateKey)
payload.public_keys = [publicKey]
const address = publicKeyToAddress(publicKey)
payload.iss = makeDIDFromAddress(address)

/* Sign and return the token */
const tokenSigner = new TokenSigner('ES256k', transitPrivateKey)
const token = tokenSigner.sign(payload) // jsontokens 用私钥签名
return token
}

最终的 Token 会成为我们看到的形式

代码语言:javascript
复制

Browser 端的参数解析与数据加载

BlockStack 浏览器端处理 query 中的 authRequest 参数

代码语言:javascript
复制
// app/js/auth/index.js 在组建内触发参数解析
componentWillMount() {
const queryDict = queryString.parse(location.search)
const echoRequestId = queryDict.echo

const authRequest = getAuthRequestFromURL() // 获取 query 中的参数
const decodedToken = decodeToken(authRequest)
const { scopes } = decodedToken.payload // gaia 授权范围

this.setState({
  authRequest,
  echoRequestId,
  decodedToken,
  scopes: {
    ...this.state.scopes,
    email: scopes.includes(&#39;email&#39;),
    publishData: scopes.includes(&#39;publish_data&#39;)
  }
})

this.props.verifyAuthRequestAndLoadManifest(authRequest) // 校验 authRequest 并获取 APP 的 Manifest.json 文件

this.getFreshIdentities() // 加载用户账户信息

}

getFreshIdentities = async () => {
await this.props.refreshIdentities(this.props.api, this.props.addresses)
this.setState({ refreshingIdentities: false })
}

// 加载用户信息
function refreshIdentities(
api,
ownerAddresses
) {
return async (dispatch) => {
logger.info('refreshIdentities')
// account.identityAccount.addresses
const promises = ownerAddresses.map((address, index) => { // 根据用户的储存在浏览器本地的地址数据循环拉取用户信息
const promise = new Promise(resolve => {
const url = api.bitcoinAddressLookupUrl.replace('{address}', address) // 比特币网络中的地址查询链接
return fetch(url)
.then(response => response.text())
.then(responseText => JSON.parse(responseText))
.then(responseJson => {
if (responseJson.names.length === 0) { // 没有用户名
const gaiaBucketAddress = ownerAddresses[0] // 默认第一个地址是用户的 gaia 地址
return fetchProfileLocations( // 寻找 profile 的储存位置
api.gaiaHubConfig.url_prefix,
address,
gaiaBucketAddress,
index
).then(returnObject => {
if (returnObject && returnObject.profile) {
const profile = returnObject.profile
const zoneFile = ''
dispatch(updateProfile(index, profile, zoneFile)) // 更新现有的用户 profile 信息
let verifications = []
let trustLevel = 0
// ...
} else {
resolve()
return Promise.resolve()
}
})
} else {
const nameOwned = responseJson.names[0]
dispatch(usernameOwned(index, nameOwned)) // 更新 redux
const lookupUrl = api.nameLookupUrl.replace('{name}', nameOwned) // 通过用户名查询数据
logger.debug(refreshIdentities: fetching: ${lookupUrl})
return fetch(lookupUrl)
.then(response => response.text())
.then(responseText => JSON.parse(responseText))
.then(lookupResponseJson => {
const zoneFile = lookupResponseJson.zonefile // 获的用户的 zonefile
const ownerAddress = lookupResponseJson.address
const expireBlock = lookupResponseJson.expire_block || -1
resolve()
return Promise.resolve()
})
.catch(error => {
dispatch(updateProfile(index, DEFAULT_PROFILE, zoneFile))
resolve()
return Promise.resolve()
})
})
.catch(error => {
resolve()
return Promise.resolve()
})
}
})
.catch(error => {
resolve()
return Promise.resolve()
})
})
return promise
})
return Promise.all(promises)
}
}

localstorage 中保存了 Redux 的数据结构

Browser 端的解析和 Manifest 拉取

image

代码语言:javascript
复制
BlockStack.js
// 校验 authRequest
export async function verifyAuthRequestAndLoadManifest(token: string): Promise<any> {
const valid = await verifyAuthRequest(token)
if (!valid) {
throw new Error('Token is an invalid auth request')
}
return fetchAppManifest(token)
}

// 校验 authRequest 的 token
export async function verifyAuthRequest(token: string): Promise<boolean> {
if (decodeToken(token).header.alg === 'none') {
throw new Error('Token must be signed in order to be verified')
}
const values = await Promise.all([
isExpirationDateValid(token),
isIssuanceDateValid(token),
doSignaturesMatchPublicKeys(token),
doPublicKeysMatchIssuer(token),
isManifestUriValid(token),
isRedirectUriValid(token)
])
return values.every(val => val)
}

// 获取 APP 的 Manifest 文件
export async function fetchAppManifest(authRequest: string): Promise<any> {
if (!authRequest) {
throw new Error('Invalid auth request')
}
const payload = decodeToken(authRequest).payload
if (typeof payload === 'string') {
throw new Error('Unexpected token payload type of string')
}
const manifestURI = payload.manifest_uri as string
try {
Logger.debug(Fetching manifest from ${manifestURI})
const response = await fetchPrivate(manifestURI)
const responseText = await response.text()
const responseJSON = JSON.parse(responseText)
return { ...responseJSON, manifestURI }
} catch (error) {
console.log(error)
throw new Error('Could not fetch manifest.json')
}
}

用户点击登录之后的授权流程

代码语言:javascript
复制
// login 函数 用户点击多个授权之后的回调
login = (identityIndex = this.state.currentIdentityIndex) => {
this.setState({
processing: true,
invalidScopes: false
})
// ...
// if profile has no name, lookupUrl will be
// http://localhost:6270/v1/names/ which returns 401
const lookupUrl = this.props.api.nameLookupUrl.replace(
'{name}',
lookupValue
)
fetch(lookupUrl)
.then(response => response.text())
.then(responseText => JSON.parse(responseText))
.then(responseJSON => {
if (hasUsername) {
if (responseJSON.hasOwnProperty('address')) {
const nameOwningAddress = responseJSON.address
if (nameOwningAddress === identity.ownerAddress) {
logger.debug('login: name has propagated on the network.')
this.setState({
blockchainId: lookupValue
})
} else {
logger.debug('login: name is not usable on the network.')
hasUsername = false
}
} else {
logger.debug('login: name is not visible on the network.')
hasUsername = false
}
}

    const appDomain = this.state.decodedToken.payload.domain_name
    const scopes = this.state.decodedToken.payload.scopes
    const needsCoreStorage = !appRequestSupportsDirectHub(
      this.state.decodedToken.payload
    )
    const scopesJSONString = JSON.stringify(scopes)
   //...
   // APP 校验权限
    if (requestingStoreWrite &amp;&amp; !needsCoreStorage) {
      this.setState({
        noCoreStorage: true // 更新跳转状态
      })
      this.props.noCoreSessionToken(appDomain)
    } else {
      this.setState({
        noCoreStorage: true // 更新跳转状态
      })
      this.props.noCoreSessionToken(appDomain)
    }
  })

}

// 跳转状态监听
componentWillReceiveProps(nextProps) {
if (!this.state.responseSent) {
// ...
const appDomain = this.state.decodedToken.payload.domain_name
const localIdentities = nextProps.localIdentities
const identityKeypairs = nextProps.identityKeypairs
if (!appDomain || !nextProps.coreSessionTokens[appDomain]) {
if (this.state.noCoreStorage) { // 跳转判断标志
logger.debug(
'componentWillReceiveProps: no core session token expected'
)
} else {
logger.debug(
'componentWillReceiveProps: no app domain or no core session token'
)
return
}
}

  // ...
  const identityIndex = this.state.currentIdentityIndex

  const hasUsername = this.state.hasUsername
  if (hasUsername) {
    logger.debug(`login(): id index ${identityIndex} has no username`)
  }

  // Get keypair corresponding to the current user identity 获得秘钥对
  const profileSigningKeypair = identityKeypairs[identityIndex]
  const identity = localIdentities[identityIndex]

  let blockchainId = null
  if (decodedCoreSessionToken) {
    blockchainId = decodedCoreSessionToken.payload.blockchain_id
  } else {
    blockchainId = this.state.blockchainId
  }
  // 获得用户的私钥和 appsNodeKey 
  const profile = identity.profile
  const privateKey = profileSigningKeypair.key
  const appsNodeKey = profileSigningKeypair.appsNodeKey
  const salt = profileSigningKeypair.salt

  let profileUrlPromise

  if (identity.zoneFile &amp;&amp; identity.zoneFile.length &gt; 0) {
    const zoneFileJson = parseZoneFile(identity.zoneFile)
    const profileUrlFromZonefile = getTokenFileUrlFromZoneFile(zoneFileJson) // 用 zonefile 获取用户信息
    if (
      profileUrlFromZonefile !== null &amp;&amp;
      profileUrlFromZonefile !== undefined
    ) {
      profileUrlPromise = Promise.resolve(profileUrlFromZonefile)
    }
  }

  const gaiaBucketAddress = nextProps.identityKeypairs[0].address
  const identityAddress = nextProps.identityKeypairs[identityIndex].address
  const gaiaUrlBase = nextProps.api.gaiaHubConfig.url_prefix
   
  // 没有 profile 就从 gaia 中查询
  if (!profileUrlPromise) {
    // use default Gaia hub if we can&#39;t tell from the profile where the profile Gaia hub is
    profileUrlPromise = fetchProfileLocations(
      gaiaUrlBase,
      identityAddress,
      gaiaBucketAddress,
      identityIndex
    ).then(fetchProfileResp =&gt; {
      if (fetchProfileResp &amp;&amp; fetchProfileResp.profileUrl) {
        return fetchProfileResp.profileUrl
      } else {
        return getDefaultProfileUrl(gaiaUrlBase, identityAddress)
      }
    })
  }

  profileUrlPromise.then(async profileUrl =&gt; {
    const appPrivateKey = await BlockstackWallet.getLegacyAppPrivateKey(appsNodeKey, salt, appDomain) // 获得 APP 的 PrivateKey
    // Add app storage bucket URL to profile if publish_data scope is requested
    if (this.state.scopes.publishData) {
      let apps = {}
      if (profile.hasOwnProperty(&#39;apps&#39;)) {
        apps = profile.apps // 获得用户的 apps 配置
      }

      if (storageConnected) {
        const hubUrl = this.props.api.gaiaHubUrl
        await getAppBucketUrl(hubUrl, appPrivateKey) // 根据用户的授权历史在 apps 查找授权 APP 的 bucket 位置,没有则创建新的
          .then(appBucketUrl =&gt; {
            logger.debug(
              `componentWillReceiveProps: appBucketUrl ${appBucketUrl}`
            )
            apps[appDomain] = appBucketUrl // bucketurl
            logger.debug(
              `componentWillReceiveProps: new apps array ${JSON.stringify(
                apps
              )}`
            )
            profile.apps = apps
            const signedProfileTokenData = signProfileForUpload(  // 更新用户的 profile
              profile,
              nextProps.identityKeypairs[identityIndex],
              this.props.api
            )
            logger.debug(
              &#39;componentWillReceiveProps: uploading updated profile with new apps array&#39;
            )
            return uploadProfile(
              this.props.api,
              identity,
              nextProps.identityKeypairs[identityIndex],
              signedProfileTokenData
            )
          })
          .then(() =&gt; this.completeAuthResponse( // 构建 AuthResponse
              privateKey,
              blockchainId,
              coreSessionToken,
              appPrivateKey,
              profile,
              profileUrl
            )
          )

      } 
      // ...
    } else {
      await this.completeAuthResponse(
        privateKey,
        blockchainId,
        coreSessionToken,
        appPrivateKey,
        profile,
        profileUrl
      )
    }
  })
} 

}

// 构造授权之后的返回

const authResponse = await makeAuthResponse(
privateKey,
profileResponseData,
blockchainId,
metadata,
coreSessionToken,
appPrivateKey,
undefined,
transitPublicKey,
hubUrl,
blockstackAPIUrl,
associationToken
)

redirectUserToApp(this.state.authRequest, authResponse)

// 重定向回 APP 页面
const payload = decodeToken(authRequest).payload
if (typeof payload === 'string') {
throw new Error('Unexpected token payload type of string')
}
let redirectURI = payload.redirect_uri as string
Logger.debug(redirectURI)
if (redirectURI) {
redirectURI = updateQueryStringParameter(redirectURI, 'authResponse', authResponse)
} else {
throw new Error('Invalid redirect URI')
}
const location = getGlobalObject('location', { throwIfUnavailable: true, usageDesc: 'redirectUserToApp' })
kk = redirectURI
}

最后我们得到

代码语言:javascript
复制
http://localhost:3000/?authResponse=eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJqdGkiOiIyMzA3NmE1NC0zYmVkLTRiYjEtOGZlOC0yY2I1MDgyNjBiOGIiLCJpYXQiOjE1ODQyNTU1MzksImV4cCI6MTU4NjkzMzg2NSwiaXNzIjoiZGlkOmJ0Yy1hZGRyOjE4d0xaanozRWFUdzd5d2o0a3hlZjR1eWlMWDFmeW9GWDMiLCJwcml2YXRlX2tleSI6IjdiMjI2OTc2MjIzYTIyNjQzNDY0NjQ2NjY0Mzg2NjM4MzAzMTM0NjQzMjYxMzY2NjM4MzAzOTM1NjEzNTY1MzIzMjMxMzY2NDYxNjM2MzIyMmMyMjY1NzA2ODY1NmQ2NTcyNjE2YzUwNGIyMjNhMjIzMDMyNjI2NDYzMzUzNjMwNjIzODY0MzY2NTM2NjEzNjY1MzEzNjM2NjEzNzM0NjQzMzM5MzYzNjM2NjUzNzYyMzMzODYxMzkzMjY1Mzg2MzMxNjIzMDM2MzY2MzY1MzAzOTY1MzUzMTM2NjIzMjYyNjE2NDYzMzIzMjM1NjMzNjMxMzMyMjJjMjI2MzY5NzA2ODY1NzI1NDY1Nzg3NDIyM2EyMjM5NjIzMzYyMzg2NDM5MzYzMjM4NjQzNjM0MzU2MTMxMzYzMzM0MzQzNTY1NjM2NTY0NjEzMzM4NjUzOTYxMzY2NjY1MzQzMTMyMzYzMjM5NjQ2MjY0NjE2NjY2NjMzNDMzMzAzNTM5NjIzNjYzNjUzNjM5NjMzMTYyMzczOTYyMzkzMzYyNjEzNTM4MzkzNjY2NjUzNzM5NjY2MTMyMzYzNjM5NjQ2NjM3MzkzMzM5MzUzNTMyNjQzNDYxMzYzNjM0Mzc2MzM5MzkzNDYyMzEzODM5MzE2NDMxMzEzNTYzMzE2NTM3MzkzNzY1MzMzMDY1MzI2NDM1MzM2NDYxMzEzODM5MzA2MTM1NjUzODMzMzc2NDM0MzUzNzY0MzIzNzM5MzI2MTMyMzMzMDY2MzgzNjYzMzkzOTMyNjQ2MjYxMjIyYzIyNmQ2MTYzMjIzYTIyNjI2NTY0MzMzMDY2MzQ2MzMyMzI2MTY1MzA2MzY0MzUzNTM1Mzc2NTYxMzQ2MzMxMzk2MjYxNjQzMzM3MzU2MjMxMzQzODM4NjEzMDM4NjQzMzY0NjIzOTMyMzQ2NTMwNjYzOTMzMzgzNjM0MzMzMDM0MzEzNTMxMzUzODM1NjQyMjJjMjI3NzYxNzM1Mzc0NzI2OTZlNjcyMjNhNzQ3Mjc1NjU3ZCIsInB1YmxpY19rZXlzIjpbIjAzOWM4OTg3OWZmNTZhMzVkOWU0MjYzYTI5ZjI5YmQyZTZjMzE1Y2UyMTZiMzYyMDZkZDA3NDg5OWMxMWJhNmRjYSJdLCJwcm9maWxlIjpudWxsLCJ1c2VybmFtZSI6Il9fY2Fvc19fLmlkLmJsb2Nrc3RhY2siLCJjb3JlX3Rva2VuIjpudWxsLCJlbWFpbCI6bnVsbCwicHJvZmlsZV91cmwiOiJodHRwczovL2dhaWEuYmxvY2tzdGFjay5vcmcvaHViLzE4d0xaanozRWFUdzd5d2o0a3hlZjR1eWlMWDFmeW9GWDMvcHJvZmlsZS5qc29uIiwiaHViVXJsIjoiaHR0cHM6Ly9odWIuYmxvY2tzdGFjay5vcmciLCJibG9ja3N0YWNrQVBJVXJsIjoiaHR0cHM6Ly9jb3JlLmJsb2Nrc3RhY2sub3JnIiwiYXNzb2NpYXRpb25Ub2tlbiI6ImV5SjBlWEFpT2lKS1YxUWlMQ0poYkdjaU9pSkZVekkxTmtzaWZRLmV5SmphR2xzWkZSdlFYTnpiMk5wWVhSbElqb2lNREprWkRKbFlXTTFaamcxTURFMllqRXlNV0kwWlRBME16SmxOREkyTkRabFlXTXhOVFkyWTJZeFpqVTVZemN4T1dZeE5UWmxPR0UyTm1ZNE1XSTRZek5pSWl3aWFYTnpJam9pTURNNVl6ZzVPRGM1Wm1ZMU5tRXpOV1E1WlRReU5qTmhNamxtTWpsaVpESmxObU16TVRWalpUSXhObUl6TmpJd05tUmtNRGMwT0RrNVl6RXhZbUUyWkdOaElpd2laWGh3SWpveE5qRTFOemt4TkRRMkxqRTROeXdpYVdGMElqb3hOVGcwTWpVMU5EUTJMakU0Tnl3aWMyRnNkQ0k2SWpjMk1UYzNZV1U1T0RWa01EVmtOell5TmpVMFltTmtZVEJsTkRReE16Y3hJbjAuMThxOTZJeW5DWTJFclRMdGtsOG05dUpQR2tickRLaXgxY3Y2NDFhOC1RRnZHT1BIODM1S0FrZm1rd19nNVRZM2lSamQxcGt3VG44Y2F1ZFhLQWY2TVEiLCJ2ZXJzaW9uIjoiMS4zLjEifQ.KGRfgjzPkQ3Y66ek2EjS2XT8EFeRc9FoElnxrsPJOCN3_YBibRpvaYPVbUkMXAqqVM6jIzlJBfdvFI3jN4O5Cg

App 端的解析与处理

app 端的数据通过 userSession.SignInPendinguserSession.handlePendingSignIn 解析 authResponse 参数

代码语言:javascript
复制
export async function handlePendingSignIn(
nameLookupURL: string = '',
authResponseToken: string = getAuthResponseToken(),
transitKey?: string,
caller?: UserSession
): Promise<UserData> {

if (!caller) {
caller = new UserSession()
}
const sessionData = caller.store.getSessionData()

if (!transitKey) {
transitKey = caller.store.getSessionData().transitKey
}
if (!nameLookupURL) {
let coreNode = caller.appConfig && caller.appConfig.coreNode
if (!coreNode) {
coreNode = config.network.blockstackAPIUrl
}

const tokenPayload = decodeToken(authResponseToken).payload
if (typeof tokenPayload === &#39;string&#39;) {
  throw new Error(&#39;Unexpected token payload type of string&#39;)
}
if (isLaterVersion(tokenPayload.version as string, &#39;1.3.0&#39;)
   &amp;&amp; tokenPayload.blockstackAPIUrl !== null &amp;&amp; tokenPayload.blockstackAPIUrl !== undefined) {

  config.network.blockstackAPIUrl = tokenPayload.blockstackAPIUrl as string
  coreNode = tokenPayload.blockstackAPIUrl as string
}

nameLookupURL = `${coreNode}${NAME_LOOKUP_PATH}`

}

const isValid = await verifyAuthResponse(authResponseToken, nameLookupURL) // 校验 authResponse Token
if (!isValid) {
throw new LoginFailedError('Invalid authentication response.')
}
const tokenPayload = decodeToken(authResponseToken).payload

// TODO: real version handling
let appPrivateKey = tokenPayload.private_key as string
let coreSessionToken = tokenPayload.core_token as string
// ...
let hubUrl = BLOCKSTACK_DEFAULT_GAIA_HUB_URL
let gaiaAssociationToken: string

const userData: UserData = {
username: tokenPayload.username as string,
profile: tokenPayload.profile,
email: tokenPayload.email as string,
decentralizedID: tokenPayload.iss,
identityAddress: getAddressFromDID(tokenPayload.iss),
appPrivateKey,
coreSessionToken,
authResponseToken,
hubUrl,
coreNode: tokenPayload.blockstackAPIUrl as string,
gaiaAssociationToken
}
const profileURL = tokenPayload.profile_url as string
if (!userData.profile && profileURL) {
const response = await fetchPrivate(profileURL) // 拉取用户 profile 信息
if (!response.ok) { // return blank profile if we fail to fetch
userData.profile = Object.assign({}, DEFAULT_PROFILE)
} else {
const responseText = await response.text()
const wrappedProfile = JSON.parse(responseText)
const profile = extractProfile(wrappedProfile[0].token)
userData.profile = profile
}
} else {
userData.profile = tokenPayload.profile
}

sessionData.userData = userData
caller.store.setSessionData(sessionData) // 缓存用户数据到

return userData // 返回结果
}

userData 最后的样子(Redux)

image

  • name - 用户的域名
  • profile - 域名下的身份信息
  • email - 用户的邮箱信息
  • decentralizedID - DID
  • identityAddress - 用户身份的 BTC 地址
  • appPrivateKey - app 应用的私钥
  • coreSessionToken - V2 预留
  • authResponseToken - browser 返回的授权信息 Token
  • hubUrl - gaia hub 的地址
  • gaiaAssociationToken - app 与 gaia 交互所需要的 token
  • gaiaHubConfig - gaia 服务器的配置信息
KeyPairs && appPrivateKey
代码语言:javascript
复制
  const appPrivateKey = await BlockstackWallet.getLegacyAppPrivateKey(appsNodeKey, salt, appDomain)

// src/wallet.ts@blockstack.js// 获取身份的密钥对

getIdentityKeyPair(addressIndex: number,
alwaysUncompressed: boolean = false): IdentityKeyPair {
const identityNode = this.getIdentityAddressNode(addressIndex)

const address = BlockstackWallet.getAddressFromBIP32Node(identityNode)
let identityKey = getNodePrivateKey(identityNode)
if (alwaysUncompressed &amp;&amp; identityKey.length === 66) {
  identityKey = identityKey.slice(0, 64)
}

const identityKeyID = getNodePublicKey(identityNode)
const appsNodeKey = BlockstackWallet.getAppsNode(identityNode).toBase58() // 获取 appsNodeKey
const salt = this.getIdentitySalt()
const keyPair = {
  key: identityKey,
  keyID: identityKeyID,
  address,
  appsNodeKey,
  salt
}
return keyPair

}
}

// 获取 appPrivateKey
static getLegacyAppPrivateKey(appsNodeKey: string,
salt: string, appDomain: string): string {
const appNode = getLegacyAppNode(appsNodeKey, salt, appDomain)
return getNodePrivateKey(appNode).slice(0, 64)
}

function getNodePrivateKey(node: BIP32Interface): string {
return ecPairToHexString(ECPair.fromPrivateKey(node.privateKey))
}

// src/storage/index.ts@blockstack.js
// 使用 appPrivateKey 加密内容
export async function encryptContent(
content: string | Buffer,
options?: EncryptContentOptions,
caller?: UserSession
): Promise<string> {
const opts = Object.assign({}, options)
let privateKey: string
if (!opts.publicKey) {
privateKey = (caller || new UserSession()).loadUserData().appPrivateKey
opts.publicKey = getPublicKeyFromPrivate(privateKey)
}
// ...
const contentBuffer = typeof content === 'string' ? Buffer.from(content) : content
const cipherObject = await encryptECIES(opts.publicKey,
contentBuffer,
wasString,
opts.cipherTextEncoding)
let cipherPayload = JSON.stringify(cipherObject)
// ...
return cipherPayload
}

// 使用 appPrivateKey 解密内容
export function decryptContent(
content: string,
options?: {
privateKey?: string
},
caller?: UserSession
): Promise<string | Buffer> {
const opts = Object.assign({}, options)
if (!opts.privateKey) {
opts.privateKey = (caller || new UserSession()).loadUserData().appPrivateKey
}

try {
const cipherObject = JSON.parse(content)
return decryptECIES(opts.privateKey, cipherObject)
} catch (err) {
}

持久化 Redux 数据
代码语言:javascript
复制
// app/js/store/configure.js@browser

import persistState from 'redux-localstorage' // 持久化 Redux 中间件

export default function configureStore(initialState) {
return finalCreateStore(RootReducer, initialState)
}

const finalCreateStore = composeEnhancers(
applyMiddleware(thunk),
persistState(null, { // persistState 持久化
// eslint-disable-next-line
slicer: paths => state => ({
...state,
auth: AuthInitialState,
notifications: []
})
})
)(createStore)

userData 也会保存在 localstorage 中

image

localstorage 的保存
代码语言:javascript
复制
export class UserSession {

// ...
constructor(options?: {
appConfig?: AppConfig,
sessionStore?: SessionDataStore,
sessionOptions?: SessionOptions }) {
// ...
if (options && options.sessionStore) {
this.store = options.sessionStore
} else if (runningInBrowser) {
if (options) {
this.store = new LocalStorageStore(options.sessionOptions)
} else {
this.store = new LocalStorageStore()
}
} else if (options) {
this.store = new InstanceDataStore(options.sessionOptions)
} else {
this.store = new InstanceDataStore()
}
}

}

// 继承并覆盖 setSessionData ,持久化数据到 LocalStorage
export class LocalStorageStore extends SessionDataStore {
key: string

constructor(sessionOptions?: SessionOptions) {
super(sessionOptions)
//...

setSessionData(session: SessionData): boolean {
localStorage.setItem(this.key, session.toString())
return true
}
}

2020-03-15
由助教曹帅整理